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.
|
survive minification.
|
||||||
4. Still-open user questions, unchanged: where the server will actually live, and
|
4. Still-open user questions, unchanged: where the server will actually live, and
|
||||||
the two account emails for `create-user.sh`.
|
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