# Plan — RSS provider Executes `architecture/rss-provider.md` against `quality/rss-provider.md`. Two commits: **(A)** the `rssdy` crate, **(B)** wiring (server feature, clients, docs). ## A — The `rssdy` crate - [x] **A1 — `api::Episode::key_for`** — `blake3(guid)` truncated to 16 hex chars. *Verifies:* G7. - [x] **A2 — `api::FeedFetcher`** — `reqwest` client with the timeout, streaming the body while counting bytes so `max_feed_bytes` rejects before buffering it whole; parse with `feed-rs`; map entries to `Episode` (guid → key, title, author, published, `itunes:duration` where present, first audio enclosure); skip entries with no enclosure; sort newest first; truncate to `limit`. Errors carry no URL. *Verifies:* G1, G15, G16, G17. - [x] **A3 — `slugify` + de-duplication** at load and on rename/subscribe. *Verifies:* G8. - [x] **A4 — `parse_path`** for root/feed/episode, rejecting anything else. *Verifies:* G10. - [x] **A5 — `Client::with_api` / `init`** — resolve subscriptions with slugs, drop entries without a url (warning), keep the settings for write-back. No feeds still mounts. *Verifies:* G18. - [x] **A6 — Listings** — `get_lib_root` lists subscriptions and is creatable; `get_lib_node("/rss/")` fetches, replaces the memo, and returns episodes as tracks (queueable, downloadable). *Verifies:* G3, G9. - [x] **A7 — Memo** — bounded push/replace, listing-written, read by `get_urls_for_track` / `get_metadata_for_track` / `resolve_tracks_into`. *Verifies:* G4, G5. - [x] **A8 — Subscription CRUD** — `create_lib_node` (URL → fetch → name from feed title → add), `rename_lib_node`, `delete_lib_node`, and `settings()` reserializing the live list. *Verifies:* G11–G14. - [x] **A9 — `rssdy/src/tests.rs`** over a `FakeFeeds` fixture backend covering every gate above. - [x] **A10 — Commit A**: crate tests, clippy, fmt clean. ## B — Wiring - [x] **B1 — Server feature `rss`** — optional `dep:rssdy`, `all-providers` membership, `ALL_PROVIDERS` + `BUILT_IN_PROVIDERS` entries, `ProviderToggles.rss`, `ProviderToggles::all`, and the mount registration through `mount_from_config`. *Verifies:* G19. - [x] **B2 — `check-features`** gains an `rss`-alone row. *Verifies:* G19. - [x] **B3 — Clients** — `/rss` into `MUTABLE_ROOTS` in `cbd-tui/src/rpc.rs` and `cbd-web/src/state.rs`, with their cacheability tests extended. *Verifies:* G6. - [x] **B4 — Docs** — `docs/src/providers/rss.md`, `rssdy/README.md`, links from `docs/src/providers.md`, `SUMMARY.md`, `config.md`, and the root README (tree + config table). Say plainly that a premium URL is a credential, that listings are never cached, and that bookmarks depend on publisher guids. *Verifies:* G20. - [x] **B5 — `plan/summary.md`** entry, then commit B. ## Deferred (recorded, not dropped) - OPML import/export, per-episode played state, background polling, and artwork (`itunes:image`) — the library model has no field for it. - Sharing the episode→`Track` mapping with `fyyd`; worth extracting only if a third podcast source lands.