Use the Google Books API key: the fallback source actually answers now

The user obtained a restricted Google Books key. Keyless requests 429 for every
caller on the internet — all anonymous traffic bills to one shared Google Cloud
project whose daily quota is permanently exhausted — so the documented fallback
has never once answered. Because MetadataRepository.combine turns "a source
failed, none found" into Unavailable, that standing failure meant every Open
Library hiccup reached the user as "couldn't be reached". The app has been
effectively single-sourced since it was written.

Build plumbing reads GOOGLE_BOOKS_API_KEY from local.properties (gitignored),
falling back to the environment and then to empty. A blank key is a supported
state: a fresh clone still builds a working app that falls back to the keyless
endpoint, rather than failing to build.

GoogleBooksClient appends the key only when non-blank, building the URL with
HttpUrl.Builder in a pure requestUrl() so it is testable without a socket. The
key is scrubbed from SourceResult.Failed.reason before that string can reach the
scan sheet — it is rendered to the user and is our only diagnostic channel from a
real phone, and some okhttp/JDK IOExceptions embed the full request URL in their
message. Defensive, not a response to an observed leak.

Resolves the RATE_LIMITED decision parked in RetryPolicy's KDoc: a keyed 429 is
the short per-user rate limit and gets exactly one retry, honouring Retry-After
capped at 2s. A keyless 429 is still the dead daily quota and is still never
retried.

Verified against the live API, not only offline: both ISBNs that failed on the
phone (9781883937386, 9781883937676) plus a control return HTTP 200, in the
percent-encoded URL shape HttpUrl actually produces. Both books are in Google
Books, so the restored fallback now covers precisely the Open Library TLS-reset
failure that broke those scans.

189 unit tests (was 172), 0 failures; Paparazzi unchanged; release APK builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J7WHnTx2Cso4VV245WDAJY
This commit is contained in:
Spriteandclaude committed 2026-09-11 23:34:43 +00:00
1 parent d6d02f788c
commit 486f6ebc48
11 files changed
+662 -37

No files matched your search

+60 -2
View File
@@ -108,7 +108,7 @@ Worth saying plainly, because it bounds how much weight the numbers carry:
## The options
### A. Give Google Books an API key
### A. Give Google Books an API key — **DONE 2026-09-11, see the end of this file**
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.
@@ -248,7 +248,8 @@ PERMANENT standing failure, so **every** Open Library hiccup became `Unavailable
The app has effectively been single-sourced this whole time while reporting
failures as though two sources had been consulted.
**The user has deliberately deferred the API key.** Do not implement it unasked.
**The user deferred the API key at the time; they asked for it on 2026-09-11 and it
is now implemented and verified.** See "The key landed" at the end of this file.
### Open Library: 13% failure, and our own timeout was manufacturing more
@@ -292,3 +293,60 @@ scanning a box of books in sequence stays on the fast path after the first book.
- The user's own 3-request sample showed 2 failures. That is consistent with 13%
(p ~ 5%) but does not confirm it. If their phone reports "3 attempts" often,
their network is worse than this one and the retry count deserves revisiting.
## The key landed — 2026-09-11
The user obtained a restricted Google Books API key and it is wired in
(commit below). This closes option A, which had been the single biggest
outstanding win in this file.
### Verified live, not just in tests
The key was tested from this machine against the real API before any code was
written, and again afterwards in the exact URL shape the app now builds:
| ISBN | Result |
|---|---|
| 9781883937386 *Hittite Warrior* | HTTP 200, 1 item |
| 9781883937676 *Shadow Hawk* | HTTP 200, 1 item |
| 9780140449136 *Crime and Punishment* (control) | HTTP 200, 1 item |
**Both of the books that failed on the user's phone are in Google Books.** They
were always in Open Library too — what actually failed was the TLS-stage
connection reset documented above, not coverage. The significance is that the
fallback would now cover exactly that failure mode: an Open Library transport
error no longer leaves the lookup with nothing to fall back to. The app has been
effectively single-sourced since it was written; it is now genuinely two-sourced.
Note the second URL test was not redundant. `HttpUrl.Builder` percent-encodes the
colon, so the app sends `q=isbn%3A9781883937386` where every hand-run test in this
file sent `q=isbn:9781883937386`. Offline unit tests cannot tell those apart and
the API accepts both — but that is a fact worth having measured rather than
assumed, because the failure mode would have been a feature that passes every
test and returns nothing on the phone.
### What was built
- The key lives in `app/local.properties` (gitignored) as `GOOGLE_BOOKS_API_KEY`,
read by `app/app/build.gradle.kts` into `BuildConfig.GOOGLE_BOOKS_API_KEY`,
falling back to an environment variable of the same name and then to empty.
**A blank key is a supported state** — a fresh clone builds a working app that
falls back to the keyless (429ing) endpoint rather than failing to build.
- `GoogleBooksClient` takes the key and appends it only when non-blank, with URL
construction in a pure `requestUrl()` so it is testable with no socket.
- **The key is scrubbed out of `SourceResult.Failed.reason`** before it can reach
the scan sheet. That string is rendered to the user and is our only diagnostic
channel from a real phone; some okhttp/JDK IOExceptions embed the full request
URL in their message, so the scrub is defensive rather than a response to an
observed leak. Do not remove it on the grounds that nothing currently leaks.
- The `RetryPolicy` RATE_LIMITED decision parked in its KDoc is now resolved: a
**keyed** 429 is the short per-user rate limit and gets exactly one retry,
honouring `Retry-After` capped at 2s; a **keyless** 429 is still the dead daily
quota and is still never retried.
### What this does NOT fix
The 17% of books with no cover art anywhere is unchanged, and so is Open
Library's ~13% transport failure rate. What changes is that a failure of one
source is now much more likely to be covered by the other instead of surfacing
as `Unavailable`.