Files
shelfmark/docs/library-check.md
splitsec2 37a77e9562 feat(library): mark search results already in a Calibre library (#1377)
Per discussion #1372, where you said you were fine with this specific
implementation: check whether metadata.db exists, read it if so, and
show a check mark saying the book is already there.

Searching for a book you already own gives no hint that you own it, so
the easiest way to end up with a second copy is to not remember you have
the first. This reads a Calibre `metadata.db`, read only, and marks
matching results with an **In library** badge in the card, list and
compact views and in the details dialog.

Off by default. It sits in Settings, General beside the existing Library
URL, with a test button that reports how many books it indexed. No HTTP
call, no token, nothing written back.

Matching runs most to least confident: a shared external id, then an
ISBN compared in both ISBN-10 and ISBN-13 form, then fuzzy title tokens
plus the author surname. The check fails open, so an unreadable database
degrades the badge and never blocks a search, and entries are cached for
ten minutes with an early refresh when the file changes, so a large
library costs one read rather than one per search.

`text_match.py` is new and shared by the index and the provider, so
title, author and ISBN matching stays consistent in one place.

## On the provider interface

`library_index` talks only to a `LibraryProvider` protocol and knows
nothing about Calibre. That is deliberate but it is not speculative
generality, it is what let me send you the Calibre half on its own: I
run an Audiobookshelf provider on the same interface in my fork, which
is where the audiobook side of the badge comes from. I have left that
out because it is a new service integration rather than something
already in the codebase, which is the line your non-goals draw. Happy to
send it separately if you ever want it, and equally happy for the answer
to be no.

Adding a library is a module with the `LibraryProvider` shape plus one
line in `all_providers()`.

## Verification

- `tests/core/test_library_index.py`: id, ISBN and fuzzy matching,
per-content-type provider selection, fail-open on provider errors,
stale-cache reuse, TTL and fingerprint refresh, per-provider cache
isolation, and the test-connection path including unsaved form values.
- `tests/core/test_text_match.py`: ISBN variants and token matching.
- `src/frontend/src/tests/libraryBadge.test.ts` and the added cases in
`bookTransformers.test.ts`.
- Python suite (3269) and frontend suite (206) green, plus ruff, ruff
format, basedpyright, vulture, tsc, oxlint, oxfmt and the production
build.
2026-09-25 18:18:10 -04:00

2.1 KiB

Library Check

Shelfmark can mark search results you already own, so you do not download a second copy of a book that is already on your shelf. The check is read only and off by default.

Calibre

Point Shelfmark at the metadata.db of a Calibre library (Calibre, Calibre-Web, Calibre-Web-Automated, anything that keeps the standard Calibre format) and it reads the database directly. No HTTP call, no API token, and nothing is ever written back.

  1. Mount the library folder into the container read only, for example /path/to/calibre-library:/calibre-library:ro. Mount the folder rather than the file so the -wal and -shm sidecars are visible, otherwise a library that is being written to can read as out of date.
  2. In Settings, General, turn on Mark books already in your Calibre library.
  3. Leave Calibre metadata.db path at /calibre-library/metadata.db unless you mounted it somewhere else.
  4. Press Test Calibre library. It reports how many books it indexed.

A result that matches the library then carries an In library badge in the card, list and compact views, and in the details dialog.

How a match is decided

In order of confidence:

  1. An external id the metadata provider and the library agree on.
  2. An ISBN, compared in both ISBN-10 and ISBN-13 form.
  3. Fuzzy title tokens plus the author surname, the same rule the rest of the app uses for book matching.

Behaviour worth knowing

  • It fails open. If the database cannot be read, Shelfmark logs a warning, reuses the last successful read if it has one, and otherwise treats the book as not owned. A broken path degrades the badge, it never blocks a search.
  • Results are cached for ten minutes, and refreshed early when the database file changes, so a large library costs one read rather than one per search.
  • Ebooks only. A Calibre library holds ebooks, so the badge answers for ebooks. The provider interface in shelfmark/core/library_providers/ takes more libraries: add a module with the LibraryProvider shape and list it in all_providers(). Nothing above that function knows which libraries exist.