crabidy/rssdy
Test User 378066470e rss: subscribe to podcast feeds at /rss, including premium ones
A new rssdy crate mounted at /rss. Subscriptions are (name, url) pairs in
rss.toml; `%` on /rss takes a pasted feed URL, fetches it once, names the
subscription from the feed's own title and persists it, `e` renames, `d`
unsubscribes without touching captured audio. Feeds are read as RSS
2.0/1.0/0.x, Atom or JSON Feed through feed-rs.

**A premium feed URL is the credential.** Library paths are displayed,
logged, and persisted into saved queues and bookmark tomls, so a URL in one
leaks into all of them. Paths therefore carry a slug of the subscription name
plus blake3(guid)[..16] — /rss/the-economist-podcasts/676f8bfa48c9cac3 — and
URLs are redacted from every Debug impl and kept out of errors (reqwest goes
through without_url).

**Nothing is cached, at either end.** A listing always fetches. The half that
is easy to miss is client-side: both clients cache listings by path and only
/crabidy, /fs and /orphans bypassed it, so /rss joins MUTABLE_ROOTS in both —
otherwise a re-visit answers from the client and the server's freshness is
invisible. One memo, written by listings and read only when resolving a track
(bounded to 8 feeds), keeps queueing 40 episodes at one fetch instead of 41
without a TTL to guess at.

Verifying against the user's real Economist feed caught a bug no unit test
would have: feed-rs parses <itunes:duration> as NPT, which has no MM:SS form,
so "53:25" fell through to its leading-number regex and a 53-minute episode
reported 53 *seconds* ("1:20:40" happens to parse fine). That field is now
recovered from the raw body — a shallow scan keyed by guid and enclosure URL —
and the live feed reports 3205/2830/1662 s, matching 53:25/47:10/27:42.

Bounded by design: per-request timeout, an 8 MiB body cap enforced while
reading chunks rather than after the fact, an episode cap, newest-first
enforced at the provider boundary so any backend obeys it. A malformed entry
is skipped; only an unfetchable feed errors, and it fails that node alone.

Behind a default-on `rss` cargo feature like every other provider, with a row
in check-features. Documented in docs/src/providers/rss.md and
rssdy/README.md, both stating plainly that the URL is a credential, that
listings are never cached, and that bookmarks depend on publisher guids —
capture what you want to keep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:36:30 +02:00
..
src rss: subscribe to podcast feeds at /rss, including premium ones 2026-07-26 12:36:30 +02:00
Cargo.toml rss: subscribe to podcast feeds at /rss, including premium ones 2026-07-26 12:36:30 +02:00
README.md rss: subscribe to podcast feeds at /rss, including premium ones 2026-07-26 12:36:30 +02:00

README.md

rssdy — the RSS podcast provider

Mounts the podcast feeds you subscribe to at /rss in the crabidy library. Works with plain public feeds and with premium per-subscriber URLs, and always shows the newest episodes.

Logging in

There is no login. A feed is either public or its URL is the credential — a paid podcast gives you a URL with a token in it:

https://feeds.economist.com/v1/rss/the-economist-podcasts/f74365b0-…

Anyone holding that URL has your subscription, so the provider treats it as a secret: it is redacted from Debug, never logged, and never put in a library path. Paths carry a slug of the subscription name and a hash of the episode id instead — paths are displayed, logged, and persisted into saved queues and bookmarks, so a URL in one would leak everywhere.

Keep rss.toml private; it holds the URLs in cleartext, like every other provider credential.

Subscribing

Either edit the config (below), or from a client: press % on /rss, paste the feed URL, and the provider fetches it once, names the subscription from the feed's own title, and writes it into rss.toml. e renames a subscription (its path changes with the name), d unsubscribes — that only removes the config entry, never audio you captured from it.

How it works

/rss
├── <subscription>          one node per feed, newest episodes first
│   └── <episode>           a track; audio is the feed's enclosure URL
└── …

A subscription is queueable and downloadable, so you can queue or W-capture a whole feed. Episodes stream directly from the enclosure URL — no sidecar, no helper binary.

Nothing is cached. Every visit to a subscription fetches the feed, so an episode published a minute ago is there. One memo exists purely so that listing a feed and then queueing its 40 episodes costs one fetch rather than 41: it is written by listings and read only when resolving a track, so it can never make a listing stale.

Episode paths are blake3(guid)[..16], which is stable as long as the publisher keeps its guids stable. Two consequences worth knowing:

  • A bookmark (w) to an episode that has since aged out of the feed cannot resolve — there is nothing left to look up. Capture (W) what you want to keep.
  • A publisher that regenerates guids on every fetch invalidates bookmarks. Nothing can be done about that from this side.

Configuration — ~/.config/crabidy/rss.toml

# One table per subscription. `name` is yours and decides the path slug;
# duplicates get a numeric suffix.
[[feeds]]
name = "The Economist Podcasts"
url = "https://feeds.economist.com/v1/rss/…"

[[feeds]]
name = "Cautionary Tales"
url = "https://feeds.example.org/cautionary-tales"

# Optional, defaults shown.
# episodes_per_feed = 200     # episodes listed per feed
# call_timeout_secs = 30      # per-request timeout
# max_feed_bytes = 8388608    # 8 MiB cap on a feed body

A feed entry with no url is skipped with a warning. No feeds at all is fine: /rss mounts empty and you can % into it.

Notes

  • Feeds are read as RSS 2.0/1.0/0.x, Atom, or JSON Feed via feed-rs.
  • itunes:duration is parsed here rather than taken from feed-rs, which reads it as NPT — a format with no MM:SS form, so 53:25 came back as 53 seconds. S, MM:SS and HH:MM:SS all work now.
  • Build the server without the rss cargo feature to leave this provider out of the binary entirely.