Skip to content

Import subscriptions from an OPML file (#63) - #74

Merged
vgururao merged 1 commit into
blygger:mainfrom
aneeshsathe:feat/opml-import
Oct 20, 2026
Merged

vgururao merged 1 commit into
blygger:mainfrom
aneeshsathe:feat/opml-import

Conversation

@aneeshsathe

Copy link
Copy Markdown
Contributor

Closes #63, built to the rulings in that thread.

The Studio. An "import OPML" button sits in the Sources subscriptions header, and the subscribe sheet has "import an OPML file…". The flow runs in one sheet:

  1. Pick a file.
  2. Preview the feeds found. Each shows its title and xmlUrl; feeds you already follow are marked and skipped. Every row has a checkbox, and there is an "add these to my blogroll" box, off by default.
  3. Import, with a status on each row: added, already subscribed, or failed with the reason.
  4. A summary of the counts.

Closing the sheet partway through doesn't stop the import; a toast reports the result.

How the file is read (src/ui/opml.ts). It's parsed in the browser with DOMParser:

  • Folders are flattened, and outlines without an xmlUrl are recursed into.
  • Duplicate feeds are dropped.
  • Includes and non-http URLs are skipped.
  • A file with bare & or HTML entities gets one retry with them escaped.

The server. The only change is an optional backfill boolean on createSubscription, default true, which is today's behaviour. With backfill: false the route skips the immediate fetch: isDue() treats a never-polled subscription as due, so the next 15-minute background poll imports it. The tests confirm that's the same work the immediate fetch does.

Pacing. One write per second, which is half the owner budget of 120 writes/min. A 429 waits out its Retry-After and retries up to 5 times; a 409 counts as already subscribed. Requests are sent with backfill: false and no title, so feed names keep following their source.

Known trade-off. A large OPML file makes one heavy background poll. Every new feed is due on the same tick, polled four at a time, and any it doesn't reach waits for later ticks, so nothing is lost.

docs/api.md gains a Subscriptions section. It documents the flag, 409/429, the blogroll default, and the write budgets, including the tighter shared budget for delegated grants (60 writes a minute across all of them).

Tests

  • tsc, including tsconfig.ui.json: clean.
  • npm test: 1748 passed, with new backfill tests. Reverting the route change makes the backfill: false test fail.
  • npm run test:ui: 107 passed. The parser and pacing-runner tests are new.
  • e2e/opml-import.spec.ts: passes on desktop and mobile, with subscription requests stubbed. It covers 201, 409, 429 then a retry, the blogroll PATCH, the 1 s spacing, and a malformed file.

🤖 Generated with Claude Code

…ption

- createSubscription takes an optional `backfill` (default true). With
  false the route skips the waitUntil backfill; isDue() treats a null
  last_poll_at as due, so the next quarter-hour cron tick imports it.
- Studio: "import OPML" on Sources and in the subscribe sheet. The file is
  parsed in the browser (DOMParser, folders flattened, type="rss" or no
  type, bare & and HTML entities repaired), previewed with already-
  subscribed rows marked, then imported one request at a time about a
  second apart, under the 120/min owner write budget. 409 counts as
  already subscribed; 429 waits out Retry-After. Optional "add these to my
  blogroll" box, off by default, uses PATCH /subscriptions/{id}.
- docs/api.md: a Subscriptions section with the flag and the budgets.
- Tests: worker (backfill false/default/invalid), UI-state parser and
  pacing runner, Playwright import flow (desktop + mobile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vgururao
vgururao merged commit 2db419e into blygger:main Oct 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: import subscriptions from an OPML file

2 participants