Found while verifying #200 against production on 2026-08-29.
Symptom
metadata.numberOfItems does not match the number of publications the feed returns:
GET /v1/api/opds?limit=200 -> numberOfItems: 96, publications: 95
GET /v1/api/opds?modified_since=… -> numberOfItems: 49, publications: 48
Consistently one short, across filters.
The real problem
The count is only how this surfaced. The actual issue is that a book in the library's collection is absent from its own catalogue — it cannot be discovered, and cannot be borrowed by any OPDS client.
Cause
Two different searches run per feed build, and only one of them filters:
LennyAPI._enrich_items → OpenLibrary.search(...) — Lenny's own class, no filtering.
LennyAPI.opds_feed → LennyDataProvider.search(...) → OpenLibraryDataProvider.search(...), whose signature is require_cover: bool = True.
With require_cover=True, pyopds2_openlibrary drops records that lack a cover image or a description:
if require_cover:
records = [r for r in records if _has_acquisition_options(r) and _has_cover(r)]
else:
records = [r for r in records if _has_acquisition_options(r)]
That default is right for Open Library's own homepage carousels — it avoids broken "Cover Unavailable" cards. It is wrong for Lenny, where the feed is not a curated shelf but the complete inventory of what this library lends. A missing cover is not a reason to hide a book the library owns.
metadata.numberOfItems comes from Item.count() — a straight DB count, correctly unaware of any of this. Hence the mismatch.
Suggested fix
Pass require_cover=False from LennyAPI.opds_feed (and search_feed, which has the same issue). _has_acquisition_options filtering should stay — a record with no way to acquire it genuinely cannot be represented in OPDS.
If some items legitimately cannot be rendered, numberOfItems should be reconciled with what is actually emitted rather than left as a raw DB count, so a harvester is not waiting on a record that will never arrive.
#200 changed numberOfItems from len(publications) (the current page's size) to Item.count() (the whole matching set). The new value is far more useful — a harvester needs the total, not the page size — but it is what made this pre-existing gap visible. The dropped book was already missing before #200; nothing reported it.
Reproducing
BASE=https://lennyforlibraries.org/v1/api
curl -s "$BASE/opds?limit=200" | jq '.metadata.numberOfItems, (.publications|length)'
To identify the missing item, diff the openlibrary_edition values in the items table against the self link ids in the feed.
Found while verifying #200 against production on 2026-08-29.
Symptom
metadata.numberOfItemsdoes not match the number of publications the feed returns:Consistently one short, across filters.
The real problem
The count is only how this surfaced. The actual issue is that a book in the library's collection is absent from its own catalogue — it cannot be discovered, and cannot be borrowed by any OPDS client.
Cause
Two different searches run per feed build, and only one of them filters:
LennyAPI._enrich_items→OpenLibrary.search(...)— Lenny's own class, no filtering.LennyAPI.opds_feed→LennyDataProvider.search(...)→OpenLibraryDataProvider.search(...), whose signature isrequire_cover: bool = True.With
require_cover=True, pyopds2_openlibrary drops records that lack a cover image or a description:That default is right for Open Library's own homepage carousels — it avoids broken "Cover Unavailable" cards. It is wrong for Lenny, where the feed is not a curated shelf but the complete inventory of what this library lends. A missing cover is not a reason to hide a book the library owns.
metadata.numberOfItemscomes fromItem.count()— a straight DB count, correctly unaware of any of this. Hence the mismatch.Suggested fix
Pass
require_cover=FalsefromLennyAPI.opds_feed(andsearch_feed, which has the same issue)._has_acquisition_optionsfiltering should stay — a record with no way to acquire it genuinely cannot be represented in OPDS.If some items legitimately cannot be rendered,
numberOfItemsshould be reconciled with what is actually emitted rather than left as a raw DB count, so a harvester is not waiting on a record that will never arrive.Note on #200
#200 changed
numberOfItemsfromlen(publications)(the current page's size) toItem.count()(the whole matching set). The new value is far more useful — a harvester needs the total, not the page size — but it is what made this pre-existing gap visible. The dropped book was already missing before #200; nothing reported it.Reproducing
To identify the missing item, diff the
openlibrary_editionvalues in theitemstable against theselflink ids in the feed.