Skip to content

joshg253/Lectio

Repository files navigation

Lectio

CI Ruff Python FastAPI WebSub Webhooks GReader API Fever API Miniflux API Last commit

Work in progress. This README covers features and design intent. Setup documentation is forthcoming.

Lectio is a self-hosted feed reader focused on fast reading triage, rich content handling, and automation. It runs well on a personal VPS with full multi-user support, and is built to keep feed reading fast, keyboard-friendly, and workflow-oriented.


What it is

A self-hosted RSS reader with a triage-first interface that adapts from a three-pane desktop layout to narrower tablet and phone workflows. Built on Python + FastAPI + the reader library, with a plain-HTML/JS frontend — no build step, no bundler, no framework.

The design priority is speed of triage: quickly marking things read, surfacing what matters, and staying out of the way.


Screenshots

Dark mode Light mode
Dark mode Light mode

More shots (settings, automation, feed properties, tags, history, admin) are in the Screenshots wiki page.


Feature highlights

Full detail lives in the wiki — Features and Multi-user & APIs. The short version:

  • Fast triage — three-pane reader, keyboard nav, context menus, bulk mark-as-read, manual tags, read history, search, and a Readability/web-view proxy.
  • Rich content — embeds that actually render (curated trusted-host allowlist), inline podcast players (incl. audio borrowed from a separate host feed), file attachments, recovered YouTube embeds, and bare-text feed cleanup. When an older article lost its player (the feed stripped the <iframe> before Lectio kept them), the missing YouTube/Bandcamp/SoundCloud embed is recovered from the source page and re-attached. Bandcamp single-track players (domain-locked to the original publisher, so they'd otherwise show "not available") fall back to the album player so they actually stream. Titles that arrive HTML-encoded (or double-encoded, as Tumblr does — Magus&rsquo; Castle) are decoded so they read correctly instead of showing the raw entity. A bare YouTube or Bandcamp album/track link sitting alone in its own paragraph (common when a feed strips the oEmbed iframe) is turned into an inline player. (Bandcamp resolves the numeric embed ID from the album page on first open and caches it; the embed appears on the next open when the page isn't yet cached.) Reader view re-injects allowlisted players (YouTube/Spotify/Bandcamp) that the readability extractor would otherwise strip — audio players (Bandcamp/SoundCloud/Spotify) keep their proper fixed height instead of a 16:9 video box — and de-duplicates a repeated lead image. YouTube embeds default to the privacy-enhanced host; a per-user Integrations setting switches them to the standard host so Share / Watch Later work, and connecting a YouTube account (per-user OAuth) adds an Add to playlist control beneath each video embed (lists your playlists, creates new ones). A global Integrations toggle auto-hides Shorts across all YouTube feeds, and a quota meter estimates your daily YouTube API usage against the cap.
  • Lead images — per-feed extraction strategies with side-by-side comparison, smart crop/fit tuning, caption sourcing, junk-image rejection, inline-SVG art, and full-resolution webcomic panels (ComicControl thumb→full promotion). List thumbnails fall back to a direct browser load when the server-side image proxy is refused (some hosts IP-block the server but serve your own IP fine).
  • Automation — highlight, mark-as-read, deduplicate, email-article, outbound-webhook (with an optional batch mode that groups all matches from one refresh run into a single {entries:[...]} request instead of one call per entry), save-to-Instapaper, add-to-YouTube-playlist, and add-to-Quire rules (the YouTube rule auto-adds new videos — including those embedded in any feed's article — to a chosen playlist, with include-Shorts, mark-read, and min/max-duration options; quota-capped, no double-adds); scope a rule to all feeds, a folder, a single feed, or a multi-selected set of feeds (deduplicate can run across a selected set of feeds, not just a whole folder), with a Duplicate button to clone one quickly; all fire at refresh time with a manual "Run Now". Starring an article can also auto-send it to Instapaper, a YouTube playlist, email, and/or Quire (Integrations → On Star).
  • Save to Pinterest — connect a Pinterest account (per-user OAuth) and a Pin button appears on each article, saving its lead image (linked back to the source) to a board you pick. Needs PINTEREST_OAUTH_CLIENT_ID/SECRET; entries without an image can't be pinned.
  • Add to Quire — connect a Quire account (per-user OAuth) and pick a destination project; an Add to Quire button then appears on each article and creates a task (titled from the entry, with the link in the description). Also available via On Star and Automation rules. Quire's per-organization minute/hour rate limits are tracked with a usage meter in Settings, and automation runs are capped and back off on a 429. Register an app at quire.io/apps/dev with redirect URI https://<your-host>/quire/callback; creds are per-user (or QUIRE_CLIENT_ID/SECRET as instance-wide fallback credentials).
  • Feed management — OPML, RSS/Atom auto-discovery, Page Feeds, YouTube & DeviantArt sync, per-folder cadence, feed compare, fetch-history & automations tabs, and duplicate-feed scanning.
  • Reliability — conditional GET, per-feed/domain backoff, GUID-churn suppression, WebSub real-time push, WAL-mode SQLite, and browser-identity fetch fallback for feeds whose servers refuse the default client.
  • Multi-user — isolated per-user databases with shared content caches; GReader, Fever, and Miniflux v1 API compatibility; Instapaper & email integrations.
  • Data portability — Takeout-style ZIP export/import and online-safe backups.

Technical overview

Layer What it does
main.py FastAPI routes, Jinja2 templates, all request handling
services/ Feed refresh, lead images, email, starred archive, YouTube, reader API wrapper
reader library Feed fetching, parsing, storage, ETag/conditional requests
lectio.db reader's SQLite feed+entry store
lectio_meta.sqlite3 App state: prefs, automation rules, lead images, read history, failure tracking
lectio_meta.sqlite Starred/saved entry archive

Stack

  • Backend: Python 3.14, FastAPI, uvicorn
  • Feed library: reader (handles HTTP, parsing, ETags, scheduling)
  • Frontend: Vanilla JS, Jinja2 templates, no build step
  • Database: SQLite (WAL mode) × 3
  • Deployment: Docker + docker-compose, Traefik reverse proxy

Development

  • Tests — pytest suite (unit, services, integration, scripts) under tests/. Run with uv run pytest.
  • CI — GitHub Actions runs the suite on Python 3.14 for every pull request and push to main (.github/workflows/ci.yml). Dependencies install from the locked uv.lock (uv sync --frozen), and the run treats any DeprecationWarning as an error so they surface immediately rather than accumulating.
  • Dependency audituv audit (OSV-backed) scans the locked dependencies for known vulnerabilities and deprecated packages. Run it locally with make audit; CI runs the same scan. It's a uv preview feature, so it's kept separate from make test locally and the CI step is informational (non-blocking) for now.

Status

Active personal use. Not yet documented for general deployment. The codebase moves fast — APIs, DB schema, and config format may change without notice.

Issues and PRs welcome, but this is primarily a personal project.

About

Self-Hosted RSS Reader

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors