axum served from inside the daemon so it reads SQLite and the event bus directly: browse feeds, read show notes, play with seeking, download and delete files, mark read/flag, and edit feed settings. Config is now hot-reloadable (Ctx.cfg behind RwLock<Arc<Config>>), so UI edits apply without a daemon restart. Access is a shared token minted from /dev/urandom, carried in a cookie because an <audio> element cannot send headers. Show notes are untrusted feed HTML and are sanitized with ammonia server-side. read/flagged finally have a writer, which retention has needed since it started ordering by them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
145 lines
6.0 KiB
Markdown
145 lines
6.0 KiB
Markdown
# 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 <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
|
|
|
|
```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 <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.
|
|
|
|
```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.
|
|
|
|
## Web UI
|
|
|
|
```toml
|
|
[web]
|
|
enabled = true
|
|
bind = "0.0.0.0:8080" # 127.0.0.1:8080 by default
|
|
token = "" # generated and written back on first run
|
|
```
|
|
|
|
`ipx daemon` then serves it in the same process (`ipx daemon --web ADDR` overrides the bind for one
|
|
run). On first start it mints a token, saves it to config.toml, and prints the URL to open:
|
|
|
|
```
|
|
web ui token generated. Open:
|
|
http://0.0.0.0:8080/?token=1f4c…
|
|
```
|
|
|
|
`?token=` sets a year-long cookie, so you only paste it once per browser. Everything is behind that
|
|
token, including `/media/...` — a cookie rather than a header precisely because an `<audio>` element
|
|
cannot send headers.
|
|
|
|
Browse feeds, read show notes, play episodes in the browser (Range requests are served, so seeking
|
|
works), download or delete individual files, mark episodes read or flag them to keep, and edit a
|
|
feed's folder/keywords/explicit/auto-download/limit settings. Config edits are written to
|
|
config.toml and hot-reloaded — no daemon restart.
|
|
|
|
Show notes are feed-supplied HTML from an untrusted source; they are sanitized with `ammonia`
|
|
server-side before they reach the page.
|
|
|
|
**It is plain HTTP.** On a LAN bind, the token and everything else crosses the network in the
|
|
clear — and a feed URL can itself contain a credential (Patreon's, for one, carries an auth token).
|
|
Put it behind a reverse proxy with TLS if that matters to you.
|
|
|
|
## 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).
|