README: a short overview that points into docs/
It described the layout from before 0.4.0 (items across the top, a player below), and carried long sections on OPML, the log view and this server's own deploy steps, all of which docs/ and CLAUDE.md cover. It now says what ipx does, how to run it with Docker or from source, the first sign-in, the TLS caveat, where the docs are, and the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173mGu6rK18Ne7UGTwAaVJV
This commit is contained in:
@@ -10,6 +10,11 @@ The long form, with what was wrong before and how it was found, is in
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The README is a short overview of what ipx does and how to run it, and points into `docs/` for
|
||||||
|
the rest. It still described the layout from before 0.4.0.
|
||||||
|
|
||||||
## [0.4.0] - 2026-09-11
|
## [0.4.0] - 2026-09-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
144
README.md
144
README.md
@@ -1,123 +1,83 @@
|
|||||||
# ipodderx-rs
|
# ipodderx-rs
|
||||||
|
|
||||||
A headless podcatcher: scans RSS/Atom feeds, downloads enclosures (HTTP and BitTorrent), files them
|
A self-hosted podcatcher for a household. It checks your feeds, downloads the episodes, and serves
|
||||||
into per-feed folders, and reaps old files to stay under a disk quota. Runs as a one-shot CLI or as
|
a web UI modelled on the 2004 Mac app **iPodderX**, for any number of people sharing one copy of
|
||||||
a daemon with a web UI, serving any number of people from one copy of the data.
|
the files. One Rust binary, `ipx`, is both the daemon and the command line.
|
||||||
|
|
||||||
## Lineage
|
It is a rewrite of [ipodderx-core](https://git.sdf1.net/rays/ipodderx-core), the Python engine
|
||||||
|
behind iPodderX (2004-2008, Ray Slakinski & August Trometer).
|
||||||
|
|
||||||
A modern Rust rewrite of [ipodderx-core](https://git.sdf1.net/rays/ipodderx-core), the Python 2
|
## What it does
|
||||||
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
|
- **The web UI.** It has a toolbar, and a feed list that opens with Directory, Popular and All
|
||||||
folders, keyword filters, the explicit-content filter, torrent enclosures, and "SmartSpace" -- the
|
Subscriptions. Items sit in a sortable table with a Files pane, and there is a player bar. It
|
||||||
oldest-first disk quota reaper.
|
comes in Dark, Light and Classic themes, and works on a phone.
|
||||||
|
- **Several people, one copy.** Each person has their own subscriptions and their own read, kept
|
||||||
|
and playback state. There is one file on disk per episode, however many people want it. People
|
||||||
|
sign in with a password or through a proxy (Cloudflare Zero Trust or Authentik), and admins
|
||||||
|
manage accounts and settings.
|
||||||
|
- **Scanning.** Feeds are checked on a schedule, globally or per feed, and a feed's own TTL is
|
||||||
|
honoured. Keyword, explicit-content and media-type filters decide what is downloaded, with a cap
|
||||||
|
on new downloads per scan.
|
||||||
|
- **Downloads.** Files come over HTTP or BitTorrent and are filed into a folder per feed.
|
||||||
|
Retention deletes the oldest files to stay under a disk quota or an age limit, and never touches
|
||||||
|
an item someone has kept.
|
||||||
|
- **OPML.** You can import and export your own subscriptions. You can also subscribe to an OPML
|
||||||
|
URL, which keeps a whole list in step as a folder.
|
||||||
|
|
||||||
What does not: iTunes and iPhoto export via AppleScript, text-to-speech enclosures, the Windows
|
## Run it
|
||||||
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
|
With Docker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t ipodderx .
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker-compose.yml` is set up for the author's own server. Point its `image` and its three volumes
|
||||||
|
(`/config`, `/data` and `/downloads`) at yours first. The UI is on port 8099. BitTorrent uses 6881
|
||||||
|
over TCP and UDP. Files are written as `PUID`/`PGID`, 99:100 by default.
|
||||||
|
|
||||||
|
From source:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build --release
|
cargo build --release
|
||||||
install -m755 target/release/ipx ~/.cargo/bin/
|
./target/release/ipx daemon
|
||||||
|
|
||||||
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
|
The first start creates **admin / ipodderx**. Sign in at `/login`, then change it:
|
||||||
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`.
|
```sh
|
||||||
|
echo -n 'a good password' | ipx user passwd admin
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI is plain HTTP, so put TLS in front of it if it is reachable from outside your network.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [docs/configuration.md](docs/configuration.md) | Every config key, paths, environment variables |
|
| [docs/configuration.md](docs/configuration.md) | Every config key, path and environment variable |
|
||||||
| [docs/cli.md](docs/cli.md) | Every command, including `ipx user` |
|
| [docs/cli.md](docs/cli.md) | Every command, including `ipx user` |
|
||||||
| [docs/users.md](docs/users.md) | Accounts, and what several people share |
|
| [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/sso.md](docs/sso.md) | Signing in through Cloudflare Zero Trust or Authentik |
|
||||||
| [docs/architecture.md](docs/architecture.md) | How it works: modules, schema, socket, HTTP API |
|
| [docs/architecture.md](docs/architecture.md) | How it works: modules, schema, control socket, HTTP API |
|
||||||
| [CHANGELOG.md](CHANGELOG.md) | What changed, by release |
|
| [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 |
|
| [docs/history.md](docs/history.md) | How it was built, with what was wrong and why |
|
||||||
| [CLAUDE.md](CLAUDE.md) | Notes for anyone (or anything) working on the code |
|
| [CLAUDE.md](CLAUDE.md) | Notes for working on the code, including how production is deployed |
|
||||||
|
|
||||||
## 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
|
## Tests
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test # the engine: parsing, filters, retention, schedules, SQL, per-user state
|
cargo test # the engine: parsing, filters, retention, schedules, SQL, per-user state
|
||||||
node tests/page-smoke.js # the page script loads without throwing
|
node tests/page-smoke.js # the page script loads without throwing
|
||||||
npx playwright test # a real browser against a real daemon
|
npx playwright test # a real browser against a real daemon on fixture feeds
|
||||||
```
|
```
|
||||||
|
|
||||||
`npm install` gets the test runner; the browser comes from
|
`npm install` gets the test runner, and `npx playwright install --with-deps chromium` gets the
|
||||||
`npx playwright install --with-deps chromium` (in `install.sh`).
|
browser.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. See [LICENSE](LICENSE).
|
MIT, see [LICENSE](LICENSE). The icons are [Font Awesome Free](https://fontawesome.com) 7.3.1 by
|
||||||
|
@fontawesome, under [CC BY 4.0](https://fontawesome.com/license/free), embedded as SVG.
|
||||||
The icons are [Font Awesome Free](https://fontawesome.com) 7.3.1 by @fontawesome, under
|
|
||||||
[CC BY 4.0](https://fontawesome.com/license/free), embedded as SVG in `web/index.html`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user