A lightweight read-only OPDS server that merges multiple Calibre libraries into a single catalog, with EPUB, PDF, CBZ, and CBR downloads for devices like the XteInk X4 running CrossPoint Reader.
A small read-only OPDS server for multiple Calibre libraries under one filesystem tree.
It scans for metadata.db files, merges supported book formats across libraries, deduplicates by title, and exposes simple OPDS feeds for ebook readers.
It is intended to work especially well with CrossPoint Reader on the XteInk X4 and similar eink devices that can consume OPDS catalogs and direct downloads.
- recursively discovers Calibre libraries under
CALIBRE_ROOT - reads
metadata.dbin read-only mode - merges all books with available EPUB, PDF, CBZ, and/or CBR files
- deduplicates entries by normalized book title, keeping the newest copy
- exposes OPDS feeds for:
- recent additions (capped for ereader compatibility)
- recently updated (capped for ereader compatibility)
- per-library views
- search results (capped for ereader compatibility)
- exposes lightweight HTML browse pages with:
- constrained cover display
- minimal metadata (series, tags, updated/published dates, short description)
- direct EPUB/PDF/CBZ/CBR download links when available
- supports pagination for browse/feed views
- keeps HTML browse/search views unbounded by the feed cap so full libraries remain browseable
- supports scored search ordering across title, authors, series, tags, and library
- serves direct EPUB/PDF/CBZ/CBR downloads when present
- optionally serves cover images
- supports optional HTTP basic auth
- includes a minimal KOReader progress sync API with SQLite-backed state
This is aimed at setups where:
- there are multiple separate Calibre libraries
- they live under one parent tree
- a reader should be able to point at one OPDS endpoint
- the main use case is: show me the most recent books and let me download them in whatever format is available
/health/libraries
/opds/opds/recent/opds/updated/opds/library/:librarySlug/opds/search?q=dune
//browse/recent/browse/updated/library/:librarySlug/search?q=dune
/download/:librarySlug/:bookId/epub/download/:librarySlug/:bookId/pdf/download/:librarySlug/:bookId/cbz/download/:librarySlug/:bookId/cbr/cover/:librarySlug/:bookId
POST /users/createGET /users/authPUT /syncs/progressGET /syncs/progress/:document
Search results are ranked primarily by title matches, then by author/series/tag/library matches, with recency as a tiebreaker. OPDS feeds stay capped to the configured feed limit; HTML browse and search views can paginate through the full result set.
cd /workspace/projects/bun-opds-server
cp .env.example .env
bun run index.tsOr with inline variables:
cd /workspace/projects/bun-opds-server
CALIBRE_ROOT=/volume1/books \
BASE_URL=http://localhost:8787 \
bun run index.ts| Variable | Default | Purpose |
|---|---|---|
CALIBRE_ROOT |
/volume1/books |
Root directory to scan for Calibre libraries |
HOST |
0.0.0.0 |
Bind host |
PORT |
8787 |
Bind port |
BASE_URL |
http://localhost:$PORT |
Public base URL used in OPDS links |
FEED_LIMIT |
100 |
Max entries in recent/updated feeds |
REFRESH_MS |
600000 |
Background refresh interval |
BASIC_AUTH_USER |
unset | Optional basic auth username |
BASIC_AUTH_PASS |
unset | Optional basic auth password |
KOSYNC_DB_PATH |
$CALIBRE_ROOT/koreader.db |
SQLite path for KOReader sync state |
bun run index.ts --help
bun run --watch index.tsTagged releases publish a container image to:
ghcr.io/rcarmo/bun-opds-server
docker run --rm -p 8787:8787 \
-e CALIBRE_ROOT=/books \
-e BASE_URL=http://localhost:8787 \
-v /path/to/books:/books:ro \
ghcr.io/rcarmo/bun-opds-server:latestA sample Compose file is included as:
docker-compose.yml
It supports PUID / PGID via Compose user mapping so the container can read host-mounted libraries as the expected user and group.
Example .env values:
PUID=1000
PGID=1000docker compose up -dA GitHub Actions workflow builds and publishes the container image on every pushed tag matching v*.
For semver tags like v0.1.0, the workflow publishes tags such as:
ghcr.io/rcarmo/bun-opds-server:0.1.0ghcr.io/rcarmo/bun-opds-server:0.1ghcr.io/rcarmo/bun-opds-server:0ghcr.io/rcarmo/bun-opds-server:latest
Example:
git tag v0.2.3
git push origin v0.2.3This project now includes a minimal compatible implementation of KOReader's progress sync API.
- stores reading progress in a SQLite database
- defaults to keeping that database at the top of the Calibre tree as
koreader.db - auto-creates users on first successful auth/sync interaction
- stores only per-user document progress state, not book files or filenames
KOReader sends auth headers:
x-auth-userx-auth-key
The sync endpoints use those directly. If a user does not already exist, the first successful request creates it. Later requests must present the same key.
Per synced document, the server stores:
document(32-character MD5 document identifier from KOReader)progresspercentagedevicedevice_id- update timestamp
This is intentionally limited to progress sync. It does not attempt to extend OPDS itself or add broader account management.
On the KOReader device, set the custom sync server URL to your server base URL, for example:
https://books.example.net- or
http://192.168.1.50:8787on a trusted LAN
The KOReader sync plugin will talk to these endpoints on that same base URL:
GET /users/authPUT /syncs/progressGET /syncs/progress/:document
Example self-hosted setup flow:
- Open Progress sync settings in KOReader.
- Enter the custom server base URL, e.g.
http://192.168.1.50:8787. - Choose a username.
- Choose a password/key.
- Let KOReader authenticate or sync once.
Because this server auto-creates users on first successful use, there is no separate admin-side provisioning step.
- intentionally read-only
- currently assumes cover images live at
cover.jpginside each Calibre book directory - cover display in HTML views is size-constrained with CSS; server-side physical resizing is still a future improvement
- currently deduplicates by normalized title and keeps the newest matching item
- search uses simple weighted scoring across title, authors, series, tags, and library name
- currently exposes a minimal OPDS 1.x-style feed aimed at ebook readers, with EPUB/PDF/CBZ/CBR acquisition links when present
- better cover detection
- server-side thumbnail generation / resizing
- deployment examples (Compose / reverse proxy)
- additional OPDS compatibility testing across readers and eink devices