Files
Jurgis Sakalauskas beb1294d86 let there be light
2026-04-28 11:48:29 +03:00

6.8 KiB

kob — article-to-EPUB for e-reader

A tiny self-hosted web app: paste an article URL → it's fetched via Jina Reader, packaged as EPUB, and listed for download. Designed to be browsed from an e-reader: HTML-only, no JS, big fonts, full-width, paginated (no scrolling).

Decisions (locked)

  • Stack: Python + Flask, ebooklib for EPUB, requests for HTTP, markdown for md→html.
  • Hosting: home server, Docker. README explains env vars and volumes.
  • Fetcher: https://r.jina.ai/<url>. Optional JINA_API_KEY env var, sent as Authorization: Bearer ... if set.
  • Articles per page: configurable via ARTICLES_PER_PAGE, default 8.
  • Delete: button per article → confirmation page (no JS) → POST to actually delete.
  • Storage: filesystem + index.json. Keep source URL and original markdown alongside the EPUB.
  • Title: taken from Jina response, used as EPUB title and slug for filename.
  • No auth: single user on home network. Document in README that this should not be exposed publicly without putting a reverse proxy + auth in front.

Storage layout

Mount one volume at /data:

/data/
  index.json                 # ordered list, newest first
  articles/
    <id>/                    # id = timestamp + slug, e.g. 20260428-153012-how-to-x
      article.epub
      source.md              # original markdown from Jina
      meta.json              # { id, url, title, fetched_at }

index.json is the source of truth for ordering and listing. Format:

{
  "articles": [
    { "id": "20260428-153012-how-to-x", "title": "How to X", "url": "https://...", "fetched_at": "2026-04-28T15:30:12Z" }
  ]
}

On delete: remove from index.json first, then rm -rf the article folder. (Order matters: if rm fails, the index is still consistent — orphan folders are harmless and can be GC'd later if ever needed.)

Routes

Method Path Purpose
GET / Main page: URL input + paginated list (?page=N).
POST /add Fetch URL → build EPUB → save → redirect to /.
GET /download/<id> Stream the .epub file.
GET /delete/<id>?page=N Confirmation page ("Delete '