Move Open Library lookup to the Editions API; stop asking Google Books for its placeholder
Two user-reported shelf-testing symptoms, both a third party answering
misleadingly and the app believing it.
1. Open Library's /api/books?bibkeys=... now 404s for EVERY ISBN, including
books OL demonstrably still holds, with OL's own x-ol-stats header on the
response while /isbn/, /search.json, /api/volumes/brief and covers.* all
serve normally. OL's docs call it the "Legacy Books API" that "may be phased
out" and it is gone from their API index, so this reads as a retirement
rather than an outage. The app had been effectively single-sourced on Google
Books since it broke: every failure the user saw was a book GB lacks.
Lookup now uses /isbn/{isbn}.json — current, non-legacy, edition-level, and
the only option of the three that carries a description. /api/volumes/brief
is a near drop-in for the old response shape and was rejected precisely
because it is also legacy.
Its costs, all handled: authors are references, so AuthorNameCache resolves
and caches them for the process lifetime (books by one author get scanned in
runs off one shelf); an edition may carry NO authors, in which case they live
on the work — 9780898707168 on the user's own shelf is exactly this, so
without the work fallback the move would have silently dropped its author;
author/work requests are best-effort and can only degrade a record, never
turn Found into Unavailable.
404 on this endpoint is authoritative NotFound. The legacy endpoint reported
a miss as 200 with an empty object, which is why every non-2xx there was a
failure. Every other non-2xx still is.
2. Google Books answers zoom=2 with a grey "image not available" PNG at HTTP
200 — not a 404 — for any volume it holds no full preview of. Coil loads it
as a success, so BookCover's placeholder never fires and the cover pipeline
uploads Google's placeholder to PocketBase as the book's cover. Measured over
18 real volumes: 11 placeholders at zoom=2, 0 at zoom=1&w=400. zoom=0/3/6 are
placeholders too. normalizeCoverUrl now pins zoom=1, adds w=400 and strips
edge=curl.
SPEC.md's "Book metadata lookup" is rewritten with both rules and the evidence
for them — it was the source of the zoom=2 instruction, and would otherwise be
the reason someone restores it.
Also fixed, because it blocked verification: LibraryViewModelTest never cleared
the view models it built, and LibraryViewModel's eleven WhileSubscribed(5_000)
flows kept running five seconds into later tests, racing resetMain(). It now
cancels each viewModelScope in tearDown.
NOT fixed, reported instead: AddBookViewModel.performSave's in-flight guard is a
check-then-act and two coroutines can both pass it. Unrelated to this change
(that VM has no metadata dependency) and out of scope. See HAZARD #13.
Verified: assembleDebug exit 0; testDebugUnitTest --rerun-tasks 378 tests,
2 skipped, 0 failures (was 355); verifyPaparazziDebug exit 0, no pixels moved;
0 "always 'false'" warnings; no build files touched. LIVE_METADATA=1 live test
ran (not skipped): 9/9 Found with cover art, with the GB key absent, so Open
Library alone answered through the new endpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4695731f91
commit
ab83287b5d
25 files changed
+972
-193
No files matched your search
+36
-3
@@ -102,14 +102,44 @@ Trigger: app start, manual pull-to-refresh, WorkManager periodic (~6h, network-c
|
||||
Never let sync failure surface as a crash or a blocking dialog — a quiet status line only.
|
||||
|
||||
## Book metadata lookup
|
||||
Primary Open Library: https://openlibrary.org/api/books?bibkeys=ISBN:{isbn}&format=json&jscmd=data
|
||||
Fallback Google Books: https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn} (no key)
|
||||
Cover: https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg else GB imageLinks (force https, zoom=2)
|
||||
Primary Open Library EDITIONS API: https://openlibrary.org/isbn/{isbn}.json (302 -> the edition record)
|
||||
Fallback Google Books: https://www.googleapis.com/books/v1/volumes?q=isbn:{isbn} (keyed)
|
||||
Merge: prefer whichever has a title; fill blanks from the other.
|
||||
|
||||
DO NOT go back to `/api/books?bibkeys=...&jscmd=data`. On 2026-09-20 it was found
|
||||
answering 404 to EVERY bibkey form for EVERY ISBN — including books Open Library
|
||||
demonstrably still holds — while /isbn/, /search.json, /api/volumes/brief and
|
||||
covers.openlibrary.org all served normally. OL's own docs call it the "Legacy Books
|
||||
API" and say it "may be phased out in the future", and it is absent from their API
|
||||
index, so treat it as retired. /api/volumes/brief (the "Legacy Partner API") is a
|
||||
near drop-in for the old response shape and is ALSO legacy — that is why it was not
|
||||
chosen. See docs/METADATA-SOURCES.md.
|
||||
|
||||
The Editions API costs extra requests, by design:
|
||||
- `authors` are REFERENCES (`{"key": "/authors/OL26320A"}`), not names; each needs
|
||||
a GET of /authors/{id}.json. `AuthorNameCache` holds resolved names for the
|
||||
process lifetime — books by one author are scanned in runs off one shelf.
|
||||
- an edition record may carry NO authors at all (real: 9780898707168), in which
|
||||
case they are on the work; fall back to /works/{id}.json. Losing this fallback
|
||||
silently drops the author for such books.
|
||||
- author/work requests are BEST-EFFORT: the edition request already succeeded, so
|
||||
a failure there degrades the record, it must never turn Found into Unavailable.
|
||||
|
||||
Cover: OL `covers: [id]` -> https://covers.openlibrary.org/b/id/{id}-L.jpg
|
||||
else GB imageLinks (force https, **zoom=1**, add `w=400`, strip `edge=curl`)
|
||||
Cover URL comes from a source that REPORTS one. Never synthesize the by-ISBN cover
|
||||
URL as if it were evidence: for an edition with no art that endpoint returns 200 +
|
||||
a 43-byte 1x1 transparent GIF, which loads "successfully" and paints nothing. As a
|
||||
last resort it may be used only with `?default=false`, which makes a miss a 404.
|
||||
OL's `covers` array uses -1 as a "no cover here" sentinel; take the first POSITIVE id.
|
||||
|
||||
DO NOT force `zoom=2` on a Google Books cover (this spec said to, and was wrong).
|
||||
For a volume Google holds no full preview of, zoom=2 is not a valid rendition and
|
||||
Google answers 200 with a grey "image not available" PNG rather than a 404 — which
|
||||
loads successfully, so BookCover's placeholder never fires and the cover pipeline
|
||||
uploads Google's placeholder to PocketBase as the book's cover. Measured 2026-09-20:
|
||||
11 of 18 real volumes did this at zoom=2; 0 of 18 at zoom=1 with a width. zoom=0/3/6
|
||||
are placeholders too, so there is no larger-zoom escape.
|
||||
|
||||
Lookup outcome is THREE-WAY, never a bare null. A source that could not be reached
|
||||
must never be reported to the user as a book that does not exist:
|
||||
@@ -117,6 +147,9 @@ must never be reported to the user as a book that does not exist:
|
||||
NotFound - EVERY source answered authoritatively and none had it
|
||||
Unavailable - no source could be reached (non-2xx, timeout, transport error)
|
||||
and none of the reachable ones had it
|
||||
On the Editions API a 404 IS authoritative NotFound — unlike the legacy endpoint,
|
||||
which reported a miss as 200 with an empty object and so made every non-2xx a
|
||||
failure. Every OTHER non-2xx remains Unavailable.
|
||||
UI: Found -> the save sheet. NotFound -> manual entry pre-filled with the scanned
|
||||
ISBN. Unavailable -> a retry affordance, with manual entry as the escape hatch;
|
||||
it must NOT claim the book is unknown.
|
||||
|
||||
Reference in new issue
Block a user