✨ Replace the Google Books API with Open Library - #102
Conversation
Open Library becomes the sole book catalogue source. It is keyless and
unmetered (Google Books required an API key and capped the free tier at
1,000 requests/day), and imposes no branding requirement, so the "Powered
by Google" marks give way to plain courtesy attribution.
- `OpenLibraryProvider` replaces `GoogleBooksProvider`: `/search.json`
(Solr) serves both search and a work's aggregate metadata, `/works/{id}
.json` the description Solr does not carry. Merged works redirect, and
details resolve to the canonical id so the cache never stores an alias.
Same-author books now key off `author_key` instead of matching a display
name, and crowd-sourced subjects are filtered down to genre-shaped ones.
- `BookSource.GOOGLE_BOOKS` → `OPEN_LIBRARY`, addressed by work id
("OL893414W"). The migration drops the cached books rather than
converting them — BookItem is an on-demand cache — along with everything
referencing one through a polymorphic (FK-less) target.
- Books lose their adult signal: Open Library exposes no maturity rating,
so `isAdult` is always false. The column and its per-account gate stay
in place for the day a source provides one.
- `MUSICBRAINZ_CONTACT` becomes `API_CONTACT`, now shared by both keyless
providers' User-Agent, and `GOOGLE_BOOKS_API_KEY` is gone.
- Fixes the seed writing its book counter under `google_books` while the
tracker wrote `googleBooks`, which left the row unlabelled on /admin/stats.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
|
The Cause: the Locally with a built The fix is one line in Generated by Claude Code |
The knip job ran `pnpm install` + `generate:paraglide` but not `pnpm build:package`, unlike lint-build-test. api/web import the shared package through its `dist/`, so without it knip couldn't resolve those imports and reported all ~215 exported DTOs in packages/shared as unused — drowning the 5 real findings and leaving the job red on every run, including on main (hidden by `continue-on-error`). Building the package first drops it to those 5, all of them genuinely dead: - `SimklWatchlistStatus`, `SimklEpisode`, `SimklSeason` are used only inside `simkl-api.types.ts` — unexported rather than removed. - `AdminVersionDto` is superseded by `RuntimeConfigDto.version`, which is what the settings footer actually reads. - `AddBookReplayDto` (shared) is superseded by the API's own validated class of the same name; the web posts an empty body. knip now exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
Branch protection can't require `docker-build`: a matrix job reports one
check per combination ("docker-build (api, apps/api/Dockerfile)"), and the
bare job name is only ever reported when the whole job is skipped. So a
rule on `docker-build` was satisfied on a push to main (where the job is
skipped in favour of docker-push) but sat at "Expected — waiting for status
to be reported" on every PR touching apps/api or apps/web, blocking
auto-merge indefinitely.
`docker-build-ok` needs docker-build, runs on always(), and passes on
success or skipped — so it always reports, under a name the matrix can't
change. Require it instead of `docker-build`.
Requiring the expanded matrix names instead would deadlock the mirror case:
a PR touching neither app skips the job, which then reports only the bare
name and never those two contexts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
Follow-up on a60bc27: adding `docker-build-ok` alongside the matrix still required flipping the branch protection rule over to the new name, and left `docker-build` sitting at "Expected — waiting for status to be reported" until that happened. Swapping the names instead means no settings change: the matrix job becomes `docker-build-images` (its legs report as "docker-build-images (api, apps/api/Dockerfile)", where a real build break stays individually visible), and the aggregating gate takes the `docker-build` name the rule already requires. It always reports, and goes red when a leg does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
Open Library becomes the sole book catalogue source, replacing the Google Books API. It is keyless and unmetered (Google Books required an API key and capped the free tier at 1,000 requests/day) and imposes no branding requirement, so the "Powered by Google" marks give way to plain courtesy attribution.
Ships as 1.6.0 (version bumped in lockstep,
CHANGELOG.mdentry added, Quackback changelog left in draft — publishing it sends the release newsletter, so that's Logan's call).The provider
OpenLibraryProviderreplacesGoogleBooksProvider, covering the same four capabilities:search(query)/search.json(Solr), explicitfieldsso the response stays smallsearchByIsbn(isbn)q=isbn:…searchByIsbns(isbns[])q=isbn:(A OR B …), batches of 20, mapped back through each doc's edition ISBN listgetDetails(id)/works/{id}.json(description) +search.json?q=key:/works/{id}(page count, publishers, subjects, rating)Notable behaviours:
type: /type/redirectstub for a work merged into another; details follow it (bounded hops) and key everything — summary, permalink, external id — off the canonical id, so the cache never stores a stale alias.author_keyandsort=rating, instead of Google's display-name matching.nyt:mass-market-monthly=2021-11-07), sentence-long entries and case variants — all dropped, capped at 10.-Lvariant (~500px) rather than Google's ~128px thumbnails.Data model
BookSource.GOOGLE_BOOKS→OPEN_LIBRARY, addressed by work id (OL893414W).BookItemis an on-demand cache keyed by the source's own id, so the migration drops the cached books rather than converting them — along with everything referencing one through a polymorphic, FK-less target that nothing cascades from: book reviews, comments, list items, activity events, book-linked notifications, and the reports/moderation decisions resolved through those reviews and comments.ReadingGoalreferences no book and is untouched.BookEntryhangs offBookItem). No production data is affected. Other domains are untouched.Trade-off accepted
Books lose their adult signal. Open Library exposes no equivalent of Google's
MATUREmaturity rating, soBookItem.isAdultis always false. The column, thefilterAdultContentcalls and the per-account gate all stay in place for the day a source provides one — documented at each site rather than removed.Config
GOOGLE_BOOKS_API_KEYremoved from.env.exampleanddocker/docker-compose.yml.MUSICBRAINZ_CONTACT→API_CONTACT, now shared by both keyless providers for the identifyingUser-Agenttheir usage policies ask for (Open Library and MusicBrainz)./admin/services: the Livres entry becomes keyless with no quota ceiling (no published limit), probed viasearch.json.Also in here
google_bookswhileQuotaTrackerServicewrotegoogleBooks— the row was rendering under its raw key instead of a label on/admin/stats.README,apps/api/README,docs/data-model.mdand the regenerateddocs/erd.md.messages/{fr,en}.json:book_google_notice→book_openlibrary_notice,settings_datasources_googlebooks_notice→…_openlibrary_notice; the Google brand mark is dropped fromprovider-brands.ts(simple-icons has no Open Library glyph, so it's listed logo-less like OMDb).Checks
pnpm test(665 passed),pnpm lint,pnpm --filter @loomkeep/web check(0 errors),pnpm knip(only pre-existing findings).🤖 Generated with Claude Code
https://claude.ai/code/session_01WqHpRYcQuZ5safw87XgVPd
Generated by Claude Code