rays d7fac2d0e7 Never list paid-feed services; scan on add; no "reaped"; slimmer header
- Security: feeds from Patreon, Supercast, Supporting Cast, Glow and
  Memberful are never listed in Popular or the Directory. A Supercast
  feed keeps its key in the URL's path, which the query check missed, so
  it was being listed.
- Adding a feed queues a scan of it, and an OPML import that added feeds
  scans what is due, so items show without pressing Scan.
- A file deleted to save space, or by hand, looks as if it was never
  downloaded: no "reaped" chip, just the Download button. The retention
  summary says "deleted".
- The feed header keeps its title and stats to one line each and wraps
  its buttons; a single feed's table drops the Feed column.
- Tests: adding a feed shows its item without Scan; a deleted file shows
  no "reaped"; paid-feed hosts and acast public ids in the unit test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
2026-09-11 14:47:03 +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 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, 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

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 Every config key, paths, environment variables
docs/cli.md Every command, including ipx user
docs/users.md Accounts, and what several people share
docs/sso.md Cloudflare Zero Trust or Authentik in front
docs/architecture.md How it works: modules, schema, socket, HTTP API
CHANGELOG.md What changed, by release
docs/history.md How it was built: the long form, with what was wrong and why
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

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

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.

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%