- Add feed lists what other accounts subscribe to, most subscribers
first, and subscribes you by id (GET /api/popular, POST
/api/popular/{id}). Rows never carry a URL. Feeds from an OPML and
anything that looks private (a login, credentials in the URL, a key
such as auth= or token=) are never listed, and the subscribe route
checks the id against the same list.
- CHANGELOG.md follows Keep a Changelog 1.1.0: 0.1.0 (2026-09-09, the
CLI), 0.2.0 (2026-09-10, the web UI), 0.3.0 (2026-09-11, accounts and
sharing). The long-form entries moved unchanged to docs/history.md.
- Cargo.toml is 0.3.0. CLAUDE.md says how to add an entry and cut a
release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
121 lines
5.9 KiB
Markdown
121 lines
5.9 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 files to stay under a disk quota. Runs as a one-shot CLI or as
|
|
a daemon with a web UI, serving any number of people from one copy of the data.
|
|
|
|
## Lineage
|
|
|
|
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), 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.
|
|
|
|
## Quick start
|
|
|
|
```sh
|
|
cargo build --release
|
|
install -m755 target/release/ipx ~/.cargo/bin/
|
|
|
|
ipx add https://atp.fm/rss # subscribe
|
|
ipx fetch # scan and download
|
|
ipx daemon # scheduler, control socket and web UI
|
|
```
|
|
|
|
On first start with `[web] enabled = true` the daemon mints a token, writes it to config.toml and
|
|
prints the URL to open. A database with no accounts starts with **admin / ipodderx** at `/login` --
|
|
change it with `echo -n '<password>' | ipx user passwd admin`.
|
|
|
|
## Documentation
|
|
|
|
| | |
|
|
|---|---|
|
|
| [docs/configuration.md](docs/configuration.md) | Every config key, paths, environment variables |
|
|
| [docs/cli.md](docs/cli.md) | Every command, including `ipx user` |
|
|
| [docs/users.md](docs/users.md) | Accounts, and what several people share |
|
|
| [docs/sso.md](docs/sso.md) | Cloudflare Zero Trust or Authentik in front |
|
|
| [docs/architecture.md](docs/architecture.md) | How it works: modules, schema, socket, HTTP API |
|
|
| [CHANGELOG.md](CHANGELOG.md) | What changed, by release |
|
|
| [docs/history.md](docs/history.md) | How it was built: the long form, with what was wrong and why |
|
|
| [CLAUDE.md](CLAUDE.md) | Notes for anyone (or anything) working on the code |
|
|
|
|
## The web UI
|
|
|
|
`ipx daemon` serves it in the same process, so it reads SQLite and the event bus directly.
|
|
|
|
Feeds down the side; the selected feed's items across the top; the selected item's text and its
|
|
enclosures below, which is where you play, download or delete them. The divider drags and its
|
|
position is remembered. Playback serves Range requests, so seeking works. An OPML subscription is a
|
|
collapsible folder whose page lists the feeds inside it.
|
|
|
|
An item may carry several enclosures; all of them appear below, and anything that is not audio or
|
|
video gets a View link rather than a player -- the publisher's copy until it is downloaded, the
|
|
local one after. Opening an item marks it read. Show notes are untrusted feed HTML, sanitized with
|
|
`ammonia` server-side before they reach the page.
|
|
|
|
The **Log** button shows the running daemon live in four tabs: *Daemon I/O* is the control protocol
|
|
itself, every command in and event out; *Scans* is feed and download activity; *HTTP* is web
|
|
requests; *All* is everything, with level and text filters and a copy button. It reads a ring buffer
|
|
held in the process, not a file, so it works the same under Docker.
|
|
|
|
It is plain HTTP. On a LAN bind the token and everything else cross the network in the clear, and a
|
|
feed URL can itself carry a credential. Put TLS in front of it if that matters.
|
|
|
|
## OPML
|
|
|
|
**Importing and exporting** a file copies subscriptions in or out once: `ipx import subs.opml`,
|
|
`ipx export subs.opml`, or Settings → Subscriptions in the UI.
|
|
|
|
**Subscribing to an OPML URL** is a live subscription, as iPodderX had. Add the OPML's URL like any
|
|
other feed; every scan re-reads it and keeps your list in step. The feeds inside are not written to
|
|
config.toml -- the OPML is the source of truth, so they are re-derived each scan and held in the
|
|
database. They show as a folder, download into one nested folder, and inherit the subscription's
|
|
settings until you change one, which gives it its own entry.
|
|
|
|
When a feed drops out of the OPML upstream, it is unsubscribed and removed -- unless it has
|
|
downloads, in which case it is kept and flagged in the UI as no longer listed. A downloaded file is
|
|
never left behind with nothing explaining where it came from.
|
|
|
|
## Docker
|
|
|
|
```sh
|
|
docker buildx build --tag 192.168.1.130:5000/ipodderx:latest . --push
|
|
docker compose pull ipodderx && docker compose up -d ipodderx
|
|
docker compose logs -f ipodderx # the first start prints the default admin password
|
|
```
|
|
|
|
`docker-compose.yml` runs the image from the registry above rather than building it, so build and
|
|
push first; change the tag in both places to use another registry. It mounts `/config` (config.toml),
|
|
`/data` (state.db) and `/downloads` from this install's host paths, which you will want to change for
|
|
yours. It publishes 8099 for the
|
|
UI and 6881 (TCP **and** UDP -- DHT needs the UDP side), and sets `PUID`/`PGID` to `99:100` so files
|
|
land owned the way Unraid shares expect. The healthcheck runs `ipx status` through the control
|
|
socket, so it catches a daemon that is alive but wedged rather than merely one that has died.
|
|
|
|
## 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.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
cargo test # the engine: parsing, filters, retention, schedules, SQL, per-user state
|
|
node tests/page-smoke.js # the page script loads without throwing
|
|
npx playwright test # a real browser against a real daemon
|
|
```
|
|
|
|
`npm install` gets the test runner; the browser comes from
|
|
`npx playwright install --with-deps chromium` (in `install.sh`).
|
|
|
|
## License
|
|
|
|
MIT. See [LICENSE](LICENSE).
|