rays 2c4d03668f Episodes can be kept on the device to listen offline (#412)
Offline mode (#389) kept lists, text and pictures, but the worker let
every /media/ request pass, so no episode played without a connection.

A downloaded episode's files pane has a keep button beside Play. It
fetches the copy that person plays, the file or, when DeadAir cuts
something for them, the version without its dead air (/media/{id}/deadair,
the save button's), whole into a cache of its own, ipx-episodes, and
asks once for persistent storage. A kept episode plays as
/media/...?offline, online or not, and the worker answers that from the
cache with the byte ranges an audio element asks for (206, 416, 200);
not kept there, it goes to the server as before. Pressing it again takes
it off; signing out clears it with everything else. Only someone's
choice is kept, so there is no budget: a full device is the quota's
refusal, said in words. The button is data-a="offline", not "keep",
which the Directory's show page already uses for subscribing.

Places saved from DeadAir's version were read in its clock only if ipx
had handed out that file's playlist since it started (state.playing). A
kept copy plays without one, so the player now names the file in the
position it saves (deadair: <enclosure>) whenever it plays DeadAir's
clock, and ipx loads that file's plan when it has none, for that item's
file only. Checked the beacon is queued by the worker offline: it is.

Tests: playing a kept episode offline, its place queued and sent once
back, and taking it off (the offline show plays quiet.mp3, the only real
MP3 among the fixtures); the worker's ranges; which copy is kept and
played, in page-unit; the server reading a flagged place in DeadAir's
clock without a playlist, and ignoring another item's file. The files
pane's one-row test counts five buttons and checks a phone's width too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqkrVDqdiQEMWAkNGQ2tYA
2026-11-07 15:31:04 +00:00
2026-09-09 19:34:25 +00:00
2026-10-17 21:55:41 +00:00

ipx

A self-hosted podcatcher for a household. It checks your feeds, downloads the episodes, and serves a web UI modelled on the 2004 Mac app iPodderX, for any number of people sharing one copy of the files. One Rust binary, ipx, is both the daemon and the command line.

It is a rewrite of ipodderx-core, the Python engine behind iPodderX (2004-2008, Ray Slakinski & August Trometer).

What it does

  • The web UI. It has a toolbar, and a feed list that opens with the Directory, Currently Listening and All Subscriptions. Items sit in a sortable table with a Files pane, and there is a player bar. It 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, pinned 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 limit on how many of a feed's newest episodes are downloaded.
  • 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 pinned.
  • 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.

Run it

With Docker:

docker build -t ipx .
docker build -f Dockerfile.web -t ipx-web .
docker compose up -d

docker-compose.yml is a sample: ipx, with its database in SQLite and its three volumes (/config, /data and /downloads) in folders beside the file, and ipx-web, the page, served by Caddy in front of it. Change them to suit. 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:

cargo build --release
./target/release/ipx daemon

The first start creates admin with a random password, written to admin-password beside config.toml (readable by ipx's user alone; the log says where, never what it is). Set IPX_ADMIN_PASSWORD to choose it instead. Sign in at /login, then change it:

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

docs/configuration.md Every config key, path and environment variable
docs/cli.md Every command, including ipx user
docs/users.md Accounts, and what several people share
docs/sso.md Signing in through Cloudflare Zero Trust or Authentik
docs/architecture.md How it works: modules, schema, control socket, HTTP API
crates/ipx/CHANGELOG.md, web/CHANGELOG.md, crates/directory/CHANGELOG.md What changed, by release, for ipx, the page and the directory
CLAUDE.md Notes for working on the code, including how production is deployed

Tests

cargo test                # the engine: parsing, filters, retention, schedules, SQL, per-user state
node tests/page-smoke.ts  # the page script loads without throwing
node tests/native-bridge.ts  # the page hands playback to a native shell
npx playwright test       # a real browser against a real daemon on fixture feeds

npm install gets the test runner, and npx playwright install --with-deps chromium gets the browser.

License

MIT, see LICENSE. The icons are Font Awesome Free 7.3.1 by @fontawesome, under CC BY 4.0, embedded as SVG.

Description
No description provided
Readme MIT 43 MiB
2026-10-24 06:03:13 -07:00
Languages
Rust 51.8%
TypeScript 28.8%
CSS 5.3%
JavaScript 5.2%
Swift 4.3%
Other 4.6%