Dukko 9a349311cf feat(metadata): add hardcover edition selection and refresh pinning (#936)
* feat(metadata): add hardcover edition selection and refresh pinning

Mirrors the StoryGraph edition picker for Hardcover, but backed by real
GraphQL edition data instead of scraping. An edition can now be chosen
either from the Hardcover settings page's new "Linked books" section or
from the metadata downloader/refresh flow, and once chosen it pins
future automatic metadata refreshes to that exact edition instead of
silently re-deriving one by ISBN every time.

book_metadata.hardcover_edition_id is shared across all users, while
hardcover_book_state.hardcover_edition_id is scoped per (user, book).
Cross-writing between them is one-directional and non-destructive per
flow: the settings page always updates the acting user's own sync
state and only back-fills the shared field if it is empty; the
metadata downloader always updates the shared field and only updates
the acting user's own sync state if they already have one. The
propagation uses the existing achievement event bus rather than a
direct module import, since HardcoverModule already depends on
BookModule.

* fix(metadata): address hardcover edition selection review findings

Responds to CodeRabbit review on PR #936:

- Surface Hardcover edition-lookup failures as errors instead of an
  empty list, so a provider outage cannot be mistaken for "no
  editions". Updates the client to throw and the UI to show a
  retryable error state.
- Validate that a manually-picked edition id actually belongs to the
  book's matched Hardcover book before persisting it, rejecting
  mismatches with a 400 instead of trusting an unvalidated client
  input.
- Require an exact safe-integer string before parsing a propagated
  hardcoverEditionId, so a value like "200abc" cannot be silently
  truncated into a valid-looking edition id.
- Add durationMs to the edition-propagation failure log per the
  project's logging conventions.
- Honor a pinned edition in the ISBN search() path too, not only in
  lookupById(), so a refresh that falls back to search still returns
  the previously chosen edition when it's present.

* test(metadata): strengthen hardcover edition-pin fallback assertions

The fallback tests for search() and lookupById() only used a
single-edition fixture, so they would have passed even if the
edition-pin filtering never ran. Use two-edition fixtures and assert
on which edition is actually selected instead.

* i18n(hardcover): use i18n keys for Linked books strings

HardcoverLinkedBooks.vue hardcoded its UI text instead of using
useI18n like its sibling components, breaking the ability to
translate the Hardcover settings page and non-English catalogs.

* fix(hardcover): harden edition selection and bound the linked-book list

Validate a picked edition against the book it belongs to rather than against a
capped, unordered list. Editions past the cap stay selectable, and two calls
can no longer disagree about which window they saw.

- Order the edition query by id and fetch one row past the cap, so truncation
  is detectable instead of silent. Verified against the live Hardcover API:
  both "The Name of the Wind" and "Pride and Prejudice" exceed the 50 cap.
- Report truncation for the linked-book list and the edition list, and say so
  in the UI rather than presenting a partial list as complete.
- Bound and order the linked-book query. It backed a settings list with the
  unbounded sync query; sync callers keep their previous unordered behaviour.
- Throw for "not connected" and "not matched" instead of returning an empty
  list, which the UI could not tell apart from a genuinely empty catalogue.
- Drop release_year from the edition query. Hardcover never populates it, so
  it was dead weight in the payload.
- Add a tuned --success token. The status pill hardcoded text-green-600, which
  is untuned in dark mode; the light value is set for 4.5:1 on card and page.
- Expose row expansion with aria-expanded and aria-controls, announce load
  failures through a live region, and keep the edition list open after a
  switch so the refreshed marker lands where the user is looking.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-08 22:35:53 -06:00
2026-07-08 01:34:42 +00:00
2026-07-08 01:34:42 +00:00

BookOrbit

A self-hosted library and reading platform for ebooks, PDFs, audiobooks, and comics.

Latest release Stars CI Release Coverage Crowdin

Website Demo GHCR Pulls Contributing License: AGPL v3

BookOrbit dashboard showing reading stats, widgets, and book shelves


What is BookOrbit?

BookOrbit organizes your books and reads them back to you anywhere: the web reader, a Kobo, or KOReader. Progress, highlights, and reading status move between all three, so you can start a chapter in one place and finish it in another.

Around that core sit 14 metadata providers, reading statistics and achievements, OPDS and Send-to-Kindle delivery, multi-user accounts with OIDC/SSO, and automatic sync out to Hardcover, Readwise, and StoryGraph. All of it runs on infrastructure you control.

Visit Website

Live Demo

Try the live instance before you install. No account required.

Launch Live Demo

Note: The demo includes a sample library of public domain books. Some administrative features are limited in the public demo. Self-hosting BookOrbit provides the full experience.

Features

Reading Experience & Sync

  • Built-in Web Readers: Ebooks (EPUB, KEPUB, MOBI, AZW3, AZW, FB2), PDFs, comics (CBZ, CBR, CB7), and audiobooks (M4B, MP3, M4A, OPUS, OGG, FLAC), with no extra plugins required.
  • Three-Way Sync (Kobo + KOReader + BookOrbit): Progress and annotations flow bidirectionally between Kobo devices, KOReader, and the BookOrbit web reader. Pick up on any surface where you left off on another, including highlights and deletions.
  • KOReader Plugin: An on-device catalog browser with search, download, and status and rating management, alongside full progress and annotation sync.
  • Annotations & Highlights: Highlights from the web reader, KOReader, and Kobo merge into one searchable hub. Filter by color, style, and source; export as Markdown, CSV, or JSON.
  • Hardcover, Readwise & StoryGraph Sync: Push status, progress, reading dates, and ratings to Hardcover on configurable triggers; status and progress to The StoryGraph; and new highlights and notes to Readwise as you create them, from both the web reader and synced devices. Hardcover read history can be pulled back to backfill blank BookOrbit entries.
  • Statistics, Goals & Achievements: Daily reading time, heatmaps, streaks, and library health, plus yearly goals, monthly challenges, and 50+ achievements across five categories. Reading DNA profiles your reading style from your actual session history.

Library Management

  • Multiple Libraries: Isolate content with per-library folders, custom scan rules, and format priorities.
  • 14 Metadata Providers: Google Books, Open Library, Amazon, Goodreads, Kobo, Hardcover, Audible, Audnexus, Libro.fm, and iTunes, plus ComicVine for comics, RanobeDB for light novels, and Aladin and Lubimyczytać for Korean and Polish catalogs. Cover art is sourced separately from iTunes, DuckDuckGo, and AudiobookCovers.
  • Smart Scopes & Collections: Organize your collection with curated lists and dynamic, rule-based saved filters.

Platform & Delivery

  • Multi-User & SSO: Granular per-user permissions and isolated reading data, with native support for Authentik, Keycloak, and Authelia via OIDC.
  • Multilingual Interface: Community translations are managed on Crowdin. See the localization guide for current language support and contributor instructions.
  • Content Delivery: OPDS support for compatible apps, Send-to-Kindle via email, and browser drag-and-drop uploads.
  • Automated Ingestion: Configure a Book Dock drop folder for hands-free importing.

Quick Start (Docker)

mkdir bookorbit && cd bookorbit
mkdir -p books data/app data/postgres
curl -fsSLo .env https://raw.githubusercontent.com/bookorbit/bookorbit/main/.env.example
curl -fsSLo docker-compose.yml https://raw.githubusercontent.com/bookorbit/bookorbit/main/docker-compose.yml

Edit .env and set these required values:

APP_URL=http://your-server-ip:3000   # the URL you'll open in your browser
BOOKS_HOST_PATH=./books              # folder on your server where your book files live

POSTGRES_PASSWORD=         # database password           - openssl rand -hex 24
JWT_SECRET=                # signs login tokens          - openssl rand -hex 32
SETUP_BOOTSTRAP_TOKEN=     # one-time setup wizard token - openssl rand -hex 16

On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set PUID and PGID to match that owner. Run id -u and id -g as the owning user to find them. Getting these wrong is the most common cause of permission errors on first scan.

Optionally set LIBRARY_BROWSE_ROOT=/books to start the library folder picker at /books instead of /.

Then start:

docker compose up -d

Open http://your-server-ip:3000 and complete setup using your SETUP_BOOTSTRAP_TOKEN.

For the full installation guide including reverse proxy setup, file permissions on NAS, external databases, and environment variable reference, see bookorbit.app/installation.

KOReader Plugin

The BookOrbit plugin for KOReader adds progress sync, two-way annotation sync, and a native catalog browser: navigate, search, and download books from your library without leaving the device.

BookOrbit KOReader Plugin showing dashboard, catalog search, and book details

  1. In BookOrbit, go to Settings > Integrations > KOReader and click Download Plugin.
  2. Unzip bookorbit.koplugin.zip.
  3. Copy bookorbit.koplugin to koreader/plugins/ on the device.
  4. Restart KOReader and open a book.
  5. Use Tools > BookOrbit Sync to connect.

The download is pre-configured with your server URL and credentials, so there is no manual entry on the device. For full setup and sync options, see bookorbit.app/koreader-plugin.

Documentation and Contributing

Full documentation is at bookorbit.app, covering libraries, metadata, readers, Kobo sync, OPDS, users and permissions, OIDC setup, and more.

For local development, see docs/DEVELOPMENT.md. To contribute, see docs/CONTRIBUTING.md for the full workflow: branch naming, test expectations, PR checklist, and commit format.

Repository Activity

BookOrbit repository activity: commits, issues, and pull requests over the last 30 days

Translations

Help translate BookOrbit into your language on Crowdin.

When adding user-facing text in code, add the Vue I18n key only to client/src/locales/en.json. Do not edit non-English catalogs in a feature pull request; untranslated keys fall back to English until Crowdin provides a translation. See docs/LOCALIZATION.md for the complete workflow.

Translation progress

Star History

BookOrbit GitHub star history

Support

License

BookOrbit is licensed under the GNU Affero General Public License v3.0.

Languages
TypeScript 74.3%
Vue 16%
Lua 7.6%
JavaScript 1.5%
CSS 0.3%
Other 0.2%