A feed catalogue, and a poller that can hold one - #2
Merged
Conversation
Imports the Kagi small-web OPML — 47,223 feeds — and adds a daemon that
keeps them current without being a resource hog.
**Why not feeds.json.** `FeedManager` loads every feed, and up to a
hundred articles per feed, into one JSON file that it reads whole on
construction and rewrites whole on every change. That is right for the
few dozen feeds a person hand-picks. For a catalogue it is quadratic
work against a file growing past a gigabyte, and it ends in an
out-of-memory crash. The store is now SQLite via `node:sqlite`, built
into Node 24, so there is no native module to compile.
Measured: the full 47,223-feed import takes 1.7s at 122 MB peak RSS.
**The daemon.** Eight feeds in flight, two seconds between batches, and
five things that make almost every poll free:
- Conditional GET. Every feed stores its ETag and Last-Modified, so an
unchanged feed costs a 304 and no parsing. Servers that ignore
validators get the same path via a content hash. Verified live: a
forced re-poll came back 176 unchanged out of 188.
- Adaptive intervals. Publishing feeds are checked sooner, quiet ones
back off to a day.
- Exponential backoff, then deactivation. Dead domains are the largest
source of waste in a catalogue this size.
- One feed per host per batch, a request timeout, and a download cap.
- Idle sleep when nothing is due.
The database is the queue — `next_fetch_at` is the cursor — so there is
no Redis, and killing the daemon mid-batch loses nothing.
**Podcasts.** `feeds harvest-podcasts` builds a podcast catalogue from
the iTunes Search API, the same keyless source `media-streamer` uses and
the one directory that hands back a real `feedUrl`. There is no bulk
export, so it is assembled from many paced searches — 2,642 unique feeds
from the first 29 queries. Resumable: results are written as they
arrive, and Ctrl-C keeps them.
**Shaped for what comes next.** Feeds are global rows and `subscriptions`
keys a feed to an account, so a follow/unfollow product adds rows rather
than needing the catalogue re-modelled. `accounts.auth_kind` leaves room
for email or CoinPay. Both tables are empty and unused for now; this CLI
is still single-user.
Live run: 25 batches polled 188 feeds and collected 3,418 articles at
~134 feeds/min, which is one full sweep of the catalogue per 6h interval.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan9 finding(s) HIGH/CRITICAL: 8 | MEDIUM: 1
Snippets are redacted; ThreatCrush never prints matched credential material. |
The 340 hand-collected `opml` feeds are the ones worth keeping — the 33k `smallweb` rows are a re-importable public catalogue. Exporting as OPML server-side keeps tens of thousands of rows out of an agent's context, which is how brisk imported them in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
marked this pull request as ready for review
August 16, 2026 10:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Imports the Kagi small-web OPML — 47,223 feeds — and adds a daemon that keeps them current without being a resource hog.
Why not
feeds.jsonFeedManagerloads every feed (and up to 100 articles each) into one JSON file it reads whole on construction and rewrites whole on every change. Right for a few dozen hand-picked feeds; for a catalogue it is quadratic work against a file growing past a gigabyte, ending in an OOM crash.Store is now SQLite via
node:sqlite— built into Node 24, so no native module to compile (this box has no build toolchain, sobetter-sqlite3was not an option).Measured: full 47,223-feed import in 1.7s at 122 MB peak RSS.
The daemon
Eight feeds in flight, 2s between batches. Five things make almost every poll free:
The database is the queue (
next_fetch_atis the cursor) — no Redis, no job server, and killing it mid-batch loses nothing.Live run: 25 batches → 188 feeds polled, 3,418 articles, ~134 feeds/min. That is one full sweep per 6h interval.
Podcasts
feeds harvest-podcastsuses the iTunes Search API — the same keyless sourcemedia-streameruses, and the one directory that returns a realfeedUrl. No bulk export exists, so it is assembled from many paced searches: 2,642 unique feeds from the first 29 queries. Resumable; Ctrl-C keeps what it found.Shaped for what comes next
Feeds are global rows and
subscriptionskeys a feed to an account, so a follow/unfollow product adds rows rather than needing the catalogue re-modelled.accounts.auth_kindleaves room for email or CoinPay. Both tables are empty and unused — this CLI is still single-user.Commands
rssamp feeds import-opml --file ~/smallweb.opml rssamp feeds harvest-podcasts [--all] rssamp feeds stats rssamp feeds recent --kind podcast rssamp daemon start [--batch 8] [--pause 2]Tests
38 new tests, all passing. Full suite is 192 passing / 11 failing — all 11 pre-existing in
config-managerandfeed-scheduler, neither of which this touches. (Note: the full suite does not terminate cleanly on its own, also pre-existing —feed-schedulerleaves real cron timers running.)🤖 Generated with Claude Code