docs: first on-device test results and metadata-source research
Records the coil3 StateFlow trap (a Kotlin warning, not an error, that silently blanked every book cover) so a future session greps for it, and the two Open Library cover defects behind it. METADATA-SOURCES.md answers the user's research question about alternative lookup sources: measurements over a 60-ISBN sample drawn from a third-party catalogue, the options with costs, and a recommendation to fix diagnosis before buying coverage. Nothing there is implemented — the user asked to be consulted first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDcPottghJXEvfYKqFM7zf
This commit is contained in:
@@ -285,3 +285,52 @@ library, scaffold — each light + dark).
|
||||
survive minification.
|
||||
4. Still-open user questions, unchanged: where the server will actually live, and
|
||||
the two account emails for `create-user.sh`.
|
||||
|
||||
## First on-device test — 2026-09-09
|
||||
The user installed the signed APK on a real phone. It runs. This closes the
|
||||
"never been run" gap that waves 1-4 all carried. Six issues came back; five were
|
||||
fixed directly by the orchestrator in commit `356f639` (they were small, and
|
||||
spinning up Sonnet workers for two-line Compose edits costs more than it saves).
|
||||
|
||||
**The one worth remembering** — `BookCover` branched on `painter.state`, but in
|
||||
coil3 that is a `StateFlow<State>`, not a `State`. Every `is
|
||||
AsyncImagePainter.State.X` arm was therefore always false, and because a `when`
|
||||
used as a statement needs no `else`, it compiled clean and drew NOTHING — no
|
||||
cover, no placeholder, no error icon. Kotlin emitted "Check for instance is
|
||||
always 'false'" as a *warning* on four consecutive lines and the build stayed
|
||||
green. **Grep the build log for `always 'false'` before accepting a wave**; that
|
||||
warning class is a silent-dead-code detector and this build had it for months.
|
||||
|
||||
Two more cover defects sat behind it, both verified against the live service:
|
||||
- `covers.openlibrary.org/b/isbn/{isbn}-L.jpg` answers **200 with a 43-byte 1x1
|
||||
transparent GIF** for an edition with no art. Any image loader calls that a
|
||||
successful load. Only `?default=false` turns a miss into a 404.
|
||||
- OL's DTO synthesized that URL unconditionally, so `MetadataMerger`'s
|
||||
fill-blanks rule could never reach Google Books' thumbnail. The SPEC'd cover
|
||||
fallback was dead code. Cover URLs now come from OL's own `cover` object.
|
||||
|
||||
Also fixed: sync bar clipped by rounded display corners (now owns its
|
||||
navigation-bar inset, wider horizontal padding, moved into Scaffold's `bottomBar`
|
||||
slot); setup screen's Password field hidden behind the IME (`safeDrawingPadding`
|
||||
outside `verticalScroll`, plus Next/Next/Done IME actions and
|
||||
`windowSoftInputMode=adjustResize`); library card titles reflowed (20sp leading,
|
||||
author gets its own 4dp gap); scan sheet now names the ISBN and says
|
||||
"Searching…" instead of showing a bare spinner.
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `./tasks/gw assembleDebug` | exit 0 |
|
||||
| `./tasks/gw testDebugUnitTest` | exit 0 — 106 tests, 1 skipped, 0 failures |
|
||||
| `./tasks/gw verifyPaparazziDebug` | exit 0 against re-recorded snapshots |
|
||||
| `./tasks/gw assembleRelease` | exit 0 — 41,777,376 bytes |
|
||||
| `apksigner verify` | V2 signer `CN=Bookshelf, O=Montanaro` — real release key |
|
||||
|
||||
### Open, not started: metadata coverage
|
||||
The user reported 1 of 3 scans resolving, and asked for **research, not a
|
||||
change**. Findings are in `docs/METADATA-SOURCES.md`. Headline: Open Library
|
||||
answered 88% of a 60-ISBN sample, keyless Google Books returned **429 on 60 of
|
||||
60** requests, and both clients collapse every non-200 into `null` — so a
|
||||
rate-limited lookup reaches the user as "No match found." Recommended order is a
|
||||
free Google Books API key, then distinguishing "couldn't ask" from "not found",
|
||||
then retry/backoff, before adding any new source. **Awaiting the user's decision;
|
||||
do not implement unasked.**
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# Book metadata lookup — why scans miss, and what else we could ask
|
||||
|
||||
Research note, 2026-09-09. Written in response to "out of the 3 barcodes I've
|
||||
scanned, only 1 has been discovered properly."
|
||||
|
||||
**Nothing in here has been implemented.** SPEC's two-source design (Open Library
|
||||
primary, Google Books fallback) is unchanged. This is the evidence for deciding
|
||||
whether to change it.
|
||||
|
||||
---
|
||||
|
||||
## Short version
|
||||
|
||||
Three separate defects were making lookups *look* far worse than the underlying
|
||||
data actually is, and all three are now fixed (commit `356f639`). They are not
|
||||
the same problem as "this book isn't in the database":
|
||||
|
||||
1. A cover that loaded fine still rendered as nothing, so a **successful** lookup
|
||||
looked like a failed one. That alone could account for the book you did find
|
||||
appearing broken.
|
||||
2. Open Library's cover URL was synthesized for every book whether or not art
|
||||
existed, and a missing cover comes back as a **200 with a 43-byte 1×1
|
||||
transparent GIF** — a "successful" load that paints nothing.
|
||||
3. Because that synthesized URL was never blank, the merge rule could never fall
|
||||
through to Google Books' thumbnail. The documented fallback was dead code for
|
||||
covers.
|
||||
|
||||
What is left is a real coverage question, and there the measurements point at one
|
||||
thing above all others: **the Google Books fallback is probably not answering at
|
||||
all.** Every keyless request from this machine returned HTTP 429, and the app
|
||||
turns any non-200 into `null`, which the UI presents as "No match found — enter
|
||||
the details by hand." A rate-limited lookup and a book that genuinely exists
|
||||
nowhere are, right now, indistinguishable to both you and me.
|
||||
|
||||
My recommendation is to fix the diagnosis before buying more data. Details in
|
||||
[Recommendation](#recommendation).
|
||||
|
||||
---
|
||||
|
||||
## What I measured
|
||||
|
||||
**Sample.** 60 ISBNs drawn from the Harvard Library catalog — deliberately a
|
||||
third party, so the sample doesn't presuppose the answer by coming from one of
|
||||
the two sources under test. Ten publishers, weighted toward the small Catholic
|
||||
and homeschool presses that a MODG family's shelf actually carries (Ignatius,
|
||||
TAN, Sophia Institute, Bethlehem Books, Baronius) alongside mainstream trade
|
||||
(Penguin, Random House, Scholastic, Crossway, Loyola).
|
||||
|
||||
**Method.** Direct HTTP against each API, one ISBN at a time, 1.2 s apart. A
|
||||
source "hits" only if it returns a usable title.
|
||||
|
||||
### Results
|
||||
|
||||
| Source | Hit | Miss | Error | Hit rate |
|
||||
|---|---|---|---|---|
|
||||
| Open Library Books API (what the app calls today) | 53 | 4 | 3 | **88%** |
|
||||
| Google Books, keyless (the app's fallback) | 0 | 0 | **60 × HTTP 429** | **0%** |
|
||||
| Harvard LibraryCloud | 56 | 4 | 0 | 93% * |
|
||||
| Open Library cover art exists for the ISBN | 47 | 13 | 0 | 78% |
|
||||
|
||||
\* Harvard is where the sample came from, so its number is inflated by
|
||||
construction. It's here to show the API works and answers by ISBN-13, not as a
|
||||
fair comparison.
|
||||
|
||||
Two further observations from the same runs:
|
||||
|
||||
- **Concurrency is punished.** The same 60 ISBNs run six-at-a-time dropped Open
|
||||
Library from 88% to 70%, entirely through transport errors. The app makes one
|
||||
request per scan, so this doesn't bite in normal use — but it does mean
|
||||
"Open Library missed" in a log is not proof the book is absent. By the end of
|
||||
this research my own IP was refused outright for a while.
|
||||
- **17% of successful Open Library lookups have no cover art at all** (9 of 53).
|
||||
Even with everything working, roughly one book in six will legitimately show
|
||||
the placeholder. That is a data fact, not a bug, and it's worth knowing before
|
||||
you read a placeholder as a failure.
|
||||
|
||||
### What this does not tell us
|
||||
|
||||
Worth saying plainly, because it bounds how much weight the numbers carry:
|
||||
|
||||
- n = 60, and the sample comes from a research library. It under-represents
|
||||
recent mass-market paperbacks, reprints and print-on-demand editions — which
|
||||
is exactly where Open Library is thinnest. Real shelf coverage is probably
|
||||
*below* 88%.
|
||||
- Every request came from a datacenter IP. The Google Books 429 may partly be
|
||||
this host sharing a quota pool with other tenants; **your phone, on a
|
||||
residential or mobile IP, may well get answers.** That's precisely why the
|
||||
app needs to be able to tell us which it got.
|
||||
- I don't know which three ISBNs you scanned. If you still have the books to
|
||||
hand, those three numbers are worth more than another 60 sampled ones.
|
||||
|
||||
---
|
||||
|
||||
## The options
|
||||
|
||||
### A. Give Google Books an API key
|
||||
Free, 1,000 requests/day, no billing account required. Turns the fallback from
|
||||
"silently 429" into a working source. Roughly a dozen lines: a key in
|
||||
`local.properties` → `BuildConfig` → `&key=` on the query.
|
||||
|
||||
The key ships inside the APK and can be extracted, so restrict it to the Books
|
||||
API in the Google Cloud console. At our volume, someone stealing it costs us
|
||||
nothing but the quota.
|
||||
|
||||
**Effort: hours. Cost: free. Likely the single biggest win.**
|
||||
|
||||
### B. Tell the difference between "not found" and "couldn't ask"
|
||||
Both clients collapse every non-200, timeout and parse failure into `null`, and
|
||||
`MetadataRepository` collapses that into "no match", and the UI writes "No match
|
||||
found." A book that's offline, rate-limited, or hit a 500 is reported to you as
|
||||
a book that does not exist.
|
||||
|
||||
Distinguishing these gets you a retry button instead of a manual-entry form, and
|
||||
gets me a real answer next time you say "it missed."
|
||||
|
||||
**Effort: half a day. Cost: free. Do this regardless of what else we choose.**
|
||||
|
||||
### C. Retry with backoff
|
||||
One retry on 429/5xx, a couple of seconds apart. Standing at a bookshelf, a
|
||||
two-second retry is invisible; a manual-entry form is not.
|
||||
|
||||
**Effort: an hour. Cost: free.**
|
||||
|
||||
### D. Add a third free source
|
||||
Only worth doing after A–C, when we can see what's actually still missing.
|
||||
Ranked by what I'd try first:
|
||||
|
||||
| Source | Key? | Notes |
|
||||
|---|---|---|
|
||||
| **Open Library `search.json`** | No | Searches the whole OL index rather than the edition table the Books API reads. Cheapest possible fallback — same service, one more request, no new failure modes. |
|
||||
| **Harvard LibraryCloud** | No | Free, no registration, answers by ISBN-13, verified working. Strong on older, scholarly and small-press books — the shape of gap we'd expect. Returns MODS; no cover art, and no ISBN-13 for pre-EAN books unless we convert. |
|
||||
| **Library of Congress** | No | The SRU endpoint (port 210) is blocked from here; the `loc.gov` JSON API responds. Excellent for US imprints. Needs more probing before I'd commit. |
|
||||
| **K10plus SRU** | No | Free German-led union catalogue, large and international. Cataloguing conventions differ enough that merging would need care. |
|
||||
|
||||
Dead ends, so nobody re-investigates them: **OCLC Classify** (retired 2021),
|
||||
**Goodreads API** (retired 2020), **Amazon Product Advertising API** (requires an
|
||||
affiliate account with qualifying sales), **WorldCat Search** (requires OCLC
|
||||
membership — institutional pricing).
|
||||
|
||||
### E. Pay for ISBNdb
|
||||
~$15–50/month depending on tier. Genuinely better coverage than anything free,
|
||||
including cover art, and a single clean API. It is also a subscription for a
|
||||
two-person home library, and I'd want proof that A–D leave a real gap before
|
||||
recommending it.
|
||||
|
||||
**Effort: hours. Cost: $180–600/year.**
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
Do **A + B + C** together — they're cheap, they're independent of any decision
|
||||
about new sources, and between them they cover the most likely explanation for
|
||||
1-in-3. Then rescan the same books and let the app tell us what it actually got.
|
||||
|
||||
If a real gap survives that, add **Open Library `search.json`** (D) as an
|
||||
in-family fallback before reaching for a new organisation's API, and Harvard
|
||||
after that.
|
||||
|
||||
I'd hold off on **E** entirely until we have numbers from your own shelf. Paying
|
||||
for coverage we might already have would be the wrong order.
|
||||
|
||||
One thing worth deciding separately: 17% of books legitimately have no cover art
|
||||
anywhere. The placeholder now looks deliberate rather than broken, but if you
|
||||
want covers on everything, that's a different feature — photograph the book,
|
||||
store it as the cover — and not a metadata-source problem at all.
|
||||
Reference in New Issue
Block a user