Skip to content

OPDS feed silently drops books that have no cover on Open Library #203

Description

@mekarpeles

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:

  1. LennyAPI._enrich_itemsOpenLibrary.search(...) — Lenny's own class, no filtering.
  2. LennyAPI.opds_feedLennyDataProvider.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.

Note on #200

#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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions