Skip to content

Keep Audiobookshelf's ASIN and ISBN as match hints (0074 book_hints) - #393

Merged
KodeStar merged 21 commits into
mainfrom
feat/abs-import-hints
Nov 14, 2026
Merged

KodeStar merged 21 commits into
mainfrom
feat/abs-import-hints

Conversation

@KodeStar

Copy link
Copy Markdown
Owner

Part of #366 (moving from Audiobookshelf ends up matched). This is the identifiers half; the certificate error, the import inside setup and the match offer follow in a second PR once #389 and #390 merge.

What

An applied Audiobookshelf import now keeps ABS's ASIN and ISBN for every ABS book it can pair with one of ours, as a match hint:

  • New path-keyed table book_hints (migration 0074: library_id, path, source, asin, isbn, import_id with 0 = none, set_at). It survives a rebuild, follows a moved folder (moveBookState) and goes with its library.
  • A hint is never the book's own ASIN, never on the player wire and never on /admin/books (tested). Nothing shows until a match run is applied.
  • Match runs and the match dialog send it: a book's own ids first, then the ABS hint, then tag hints. A tag ASIN goes only when the import gave neither identifier (metaserve asks the ASIN first and stops on a hit, so a tag ASIN beside an ABS ISBN would outrank the import). A known ASIN whose length, title and series agree matches confidently and is recorded the normal way (book_matches how=auto); a disagreeing one goes to review.
  • Pairing: path tier and title tier, every ABS book item, all-access. Contested and gone items are skipped; books already matched are counted, not hinted. Undo removes that import's hints; the last applied import owns them.
  • Every import carries them. The review's summary gains an additive identifiers block (books, asin, isbn, by_title, already_matched), shown as a review row; the stored payload gains identified (omitempty, payload version unchanged).
  • An import that brings identifiers but no listening for the person can now be applied ("No listening to import"), unless it would undo an import already applied for them: then Apply is off with a warning, and the server refuses with 409 import_not_ready and changes nothing. "Listening" matches the console's rule exactly (sessions, days, bookmarks, progress that is new or moves position, finished or finish date).
  • The console's note on what matching sends now names an ABS import.
  • The LibriVox placement clears a deleted book's hint along with its other leftovers.

Tests

Catalog apply/undo/re-import/move/library delete/rebuild of hints, the refusal (and that the applied import's rows are intact), the listening rule table, pairing tiers and skips, plan summary counts, QueryFor order, an ABS-hinted identifier hit confident vs disagreeing, migration 0074, the API's identifier-only apply and refusal, hints absent from /item and /admin/books, admin-only import routes. Console: identifiers row, identifier-only notice, disabled Apply + warning.

Gate: scripts/build-admin.sh, go build/vet/test -race ./..., golangci-lint run (0 issues), native-tag vet/test -race/lint.

Reviews: one ★ review (two passes), one fix round.

Verified live

Scratch server from this branch against a real Audiobookshelf 2.37.2 in Docker and live meta.audiosilo.app, four tagless books with no metadata.json:

  1. Import with listening: review shows "ASIN or ISBN for matching: 4 books"; after Apply, 4 book_hints rows, books.asin empty, no hint in /item or /admin/books.
  2. Match automatically: 4 confident, applied as book_matches how=auto on the hinted recordings (two only cleared the auto bar through the identifier hit).
  3. An ASIN meta doesn't know: no identifier reason, matched by title/author/length to the right recording, never the fake ASIN.
  4. Undo removes the hints; a new import writes them again; rescan keeps them; a moved folder's hint follows its new path.
  5. Identifier-only: a user with nothing applied gets the notice and Apply works; a user with an applied import sees Apply off and the warning, a direct POST gets 409 import_not_ready, their progress byte-identical and their import still applied.

Notes

…_hints)

An applied listening import can now write the ASIN and ISBN Audiobookshelf
has for a book into book_hints (source abs, owned by the import), never
into books.asin/isbn, so a hinted book stays unmatched until a match run or
the dialog matches it. The rows are normalized and validated, replaced whole
by the last import applied, deleted by that import's undo (and by a
re-import's taking back of the previous one), carried by a move and a join,
kept by a rebuild and dropped with their library. A match run's subjects and
GetBook read the hint; ImportBooks says whether each book is already matched.

Part of #366.
matchrun.QueryFor now sends, for a book with no ASIN or ISBN of its own,
each identifier field from the Audiobookshelf import's hint first and the
tag's second, so a match run and the match dialog look ABS's identifier up.
A hit stays confident only while the book's length, title and series agree
with it, and is held as identifier_disagrees otherwise, as tag hints are.

Part of #366.
The fetch keeps every ABS book with a valid identifier (payload
"identified", additive; payload v stays 1), and the review, a cutoff change
and the apply pair them with books here for the whole server, by path then
title and author, skipping gone items, split discs, contested books and
books already matched. The apply writes them as match hints owned by the
import, and the summary gains an additive "identifiers" block (books, asin,
isbn, by_title, already_matched) so the review says what goes over.

Part of #366.
The import review lists "ASIN or ISBN for matching" with the number of
books, how many were paired by title and author and how many were already
matched, when the summary has any; an import planned before the server
counted them shows no row. Six locales.

Part of #366.
CLAUDE.md's data model gains 0074 and the importer line its identifiers;
CONVENTIONS' Listening imports describes the pairing, ownership and undo,
and Tag identifiers are hints the order QueryFor sends them in.

Part of #366.
An Audiobookshelf user whose history matches no book here can still carry
ABS's ASIN and ISBN for the server's books as match hints, but the console
called that import "Nothing to import" and kept Apply off. It now says the
apply brings over identifiers for N books as match hints with no listening,
enables Apply with a matching confirmation and toast, and keeps "Nothing to
import" for an import with neither history nor identifiers. The server never
refused an empty plan; a test applies and undoes such an import end to end.

Part of #366.
A newly placed LibriVox book already loses the ASIN and ISBN a deleted book left at its path (overrides and enrichment). An Audiobookshelf hint left there was a third place they lingered: the match dialog sends a hint for a book with no identifier of its own, so it would have looked the new recording up by the old book's ASIN. The hint goes with the rest; an adopted folder keeps its own.

Part of #366.
…saction

The pairing of Audiobookshelf's identified books with the books here reads nothing of a user's, yet it ran once per mapped user in a fetch and, on apply, inside ApplyImport's transaction, which holds the server's one writer: with no shared folders every ABS book goes to the title tier, seconds of work at tens of thousands of books while every progress save waits. A fetch now pairs once for all its users and an apply pairs beside the history's matching, before the transaction.

The identified list is built with the item index (it is the same for every user), and the check for a gone item is dropped: only items ABS still has are ever listed. A cutoff change still counts the identifiers (tested).

Part of #366.
Every other import-owned table (0034) marks 'no import' with 0, and code reads them with import_id = 0 / <> 0, which silently skips a NULL. Nothing writes a NULL here, so the column takes the same convention before the schema ships. The index is no longer partial: every row is an import's, and a bound import_id = ? cannot use an index WHERE import_id <> 0.

Part of #366.
…s once

GetBook bound the hint's source as a second positional parameter while the match run's join inlined it: both now inline the constant, so GetBook takes one argument again. The console's review asked 'identifiers only?' and then read the count again with a fallback that could not fire: the model returns the count. The catalog's import tests share one apply helper.

Part of #366.
BookMatchState and coverExpected reuse the match subject's join for the recorded match alone, so extending that join with book_hints tied two reads that never look at a hint to the hint table. The book_matches join is its own constant; the subject's join is that plus the hint.

Part of #366.
Three comments predated the hint or left a case out: the match dialog's lookup named only tag hints; the unknown-ASIN report cites QueryFor's rule but reads a book's tag ASIN only, never its Audiobookshelf hint; and a summary's identifiers are all zero, not absent, once an import fetched before them is planned again.

Part of #366.
A hint is what a match sends, never the book's identifier: the import test now reads the hinted book through /item and the console's book list and checks neither response carries the hint's ASIN.

Part of #366.
A stored payload no longer names only the books a person has history for (its identified list is the whole ABS server's); a disc join copies a match hint whole from the earliest part that has one, not field by field; and a run's identifier is a hint from an import or a tag, not always a tag's.

Part of #366.
QueryFor mixed the hint sources field by field, so a book with an Audiobookshelf ISBN and a tagged ASIN sent both. metaserve asks the ASIN first and stops when it knows it (audiosilo-meta pkg/query match), so the tag's ASIN, the hint that is wrong about one time in six, was looked up ahead of the import's identifier and the ISBN never asked: the tag outranked the import, the reverse of the order the hint exists for.

A tag's ASIN is now sent only when the import gave nothing. A tag's ISBN still goes beside an import's ASIN, as it is asked second. The cost: an import's ISBN the community doesn't know no longer falls back to the tag's ASIN (the book goes to a title match).

Part of #366.
The console tells the admin what matching sends to the community service in three places (Settings > Metadata, the bulk match card, a held item's reason), and each said an ASIN or ISBN comes from the book's tags. After an import a match sends Audiobookshelf's identifiers too, for books whose files carry none, so the disclosure names both in all six locales.

Part of #366.
An apply takes back the person's previous applied import from the same
source first. Since identifier-only imports can be applied, one with no
listening for a person who already had their history imported would take
that history back and write none, and undoing it would not bring it back.

ApplyImport now refuses a write with no session, day, progress or bookmark
when there is a previous applied import to take back
(ErrImportNoListening, 409 import_not_ready, the transaction rolled back
so nothing is written and the import stays in review). An identifier-only
import for a person with nothing applied still applies and writes its
hints. The console turns Apply off in that case and says to undo the
applied import first to replace it.

Part of #366.
An import's ISBN brings its own pair (its ASIN, if any, beside it);
without one, the import's ASIN or else the tag's goes with the tag's
ISBN. Same queries as before, read off one if/else instead of an
assignment overwritten by a second check. Rewraps the CONVENTIONS
line that says so.

Part of #366.
The apply and the keeps-Apply-off tests pair on the same summary, so
they read it from one const.

Part of #366.
ApplyImport counted any progress row as listening, but the importer
also writes a row whose only change is an earlier start date, which
the review's summary (and so the console's historyEmpty) does not
count. Such a re-import showed Apply off in the console, yet a direct
POST was accepted and took back the person's applied import, sessions
and all, for a start-date tweak. A progress row now counts only when
it is new or moves the position, finish or finish date, matching the
summary.

Part of #366.
@KodeStar
KodeStar merged commit 41f20d0 into main Nov 14, 2026
4 checks passed
@KodeStar
KodeStar deleted the feat/abs-import-hints branch November 14, 2026 18:10
KodeStar added a commit that referenced this pull request Nov 14, 2026
…etup, offer Match automatically (#395)

Part 1 of #366 (Audiobookshelf import). Part 2, the identifiers kept as
match hints, merged as #393.

## What
- **`abs_certificate`**: when Audiobookshelf's https certificate isn't
trusted (self-signed or a private CA), Connect, Status and a background
fetch now fail with `abs_certificate` (still 502) instead of
`abs_unreachable` / `not_abs`. The console shows it under the address
field and points at ABS's plain http:// address. Nothing is ever trusted
or skipped. Detection is one `errors.As` on
`*tls.CertificateVerificationError`: Go's client handshake wraps every
verify failure (unknown authority, hostname, invalid) in that type.
https to an http-only port stays `abs_unreachable`
(`http.ErrSchemeMismatch`).
- **Import inside setup**: `ImportFlow` (connect, map, review, history)
is extracted from Settings > Import and rendered inline in the setup
flow's Audiobookshelf step (`embedded`), replacing the new-tab link.
Continue and Skip stay. While a books library is scanning, the step says
"Wait for the scan to finish" and Connect is off (from the libraries the
flow already polls, so no wire field).
- **Match offer**: #390's `UnmatchedPrompt` (Match automatically)
appears once an import is applied, in the Past imports card, and in the
setup step once the scan is done. It links to Health (a new tab from
setup) and never starts a run by itself.
- #393's identifiers row and the identifier-only apply guard are
unchanged inside the extracted flow.

Error-code wire change is additive; no player change.

## Tests
Go: a TLS fake gives `abs_certificate` from Status and Connect. A
hostname mismatch with a trusted pool gives it too, and 127.0.0.1
connects as the control. https to an http port stays `abs_unreachable`,
and plain http is unchanged. API: 502 on connect, and `error_code` on a
failed background fetch. Console: the certificate row, ImportFlow inline
in the abs step with the scan gate, the offer after an apply and held
until the imports are read, and imports-model. Gate: build-admin.sh
(1628 tests), go build/vet/test -race, golangci-lint 0.

## Verified live
Real Audiobookshelf 2.37.2 in Docker:
- **Certificate error**: http connects. Behind a private-CA proxy and a
truly self-signed proxy, Connect shows the certificate message under the
address field, and curl returns 502 `abs_certificate`. Switching to
http:// connects. The same results came from the Linux binary inside the
`:edge` image (Alpine, Go's own verifier).
- **Setup step**: on a fresh server with a slowed first scan (97 s), the
gate showed and lifted 0.8 s after the scan ended. Connect, map, fetch,
review (the identifiers row read "4 books") and apply all ran inline.
Exactly one offer appeared, in Past imports, and it opened Health in a
new tab with no match run started. Continue and Skip both moved on.
- **Layout**: no sideways scroll at 390 px or on desktop.

Not verified: the image's native-SQLite (CGO) build. The certificate
check runs before any database work.

## Notes
- de/es/fr/it/pt text for `errors.absCertificate`,
`imports.failure.abs_certificate`, `setup.abs.import` and
`setup.abs.waitScan` is machine-translated and wants a native read.
- Follow-ups go in one issue (scan gate covers Connect only;
no-books-library wording; redirect-to-https hint; offer link icon; the
map table is cramped on a phone; two Back buttons while mapping).

Pairs with KodeStar/audiosilo-docs (docs for #366, PR to follow).
KodeStar added a commit to KodeStar/audiosilo-docs that referenced this pull request Nov 14, 2026
…ort inside setup (#310)

Docs for KodeStar/audiosilo-server#366, the Audiobookshelf import. It
covers part 2 (KodeStar/audiosilo-server#393, merged) and part 1
(KodeStar/audiosilo-server#395). **Merge with or after #395**: the
certificate error, the import inside setup, the match offer and
`setup/abs.png` exist only there.

## User Guide
- **admin/import.md**: a new "Match your books after an import" section.
An import's ASINs and ISBNs are match hints. They never become the
book's ASIN and never show in the apps, and the book stays unmatched
until a match is applied (hence the offer). Hints come only from
libraries the token can see. It also covers:
  - undo, and which import owns the hints;
- an identifier-only import refused when it would replace an applied
one;
- the certificate error, with the advice to use Audiobookshelf's plain
http:// address (never trust the certificate);
  - waiting for the scan.
- **getting-started/first-run.md**: step 6 now runs the import inside
the step, says to wait for the scan, and shows the Match automatically
offer.
- **getting-started/coming-from-audiobookshelf.md**: matches the above.
ABS's "Store metadata with item" is off by default
(`server/objects/settings/ServerSettings.js`, `storeMetadataWithItem =
false`, v2.37.2), so that line is unchanged.
- **admin/health.md**: a new "Audiobookshelf's ASINs and ISBNs" section.
A `metadata.json`'s ids are the book's own; an import's are hints. A
wrong hint is settled by matching the book by hand.
- **admin/books.md** ("What matching sends") and **features.md**.

## Developer Docs
- **api/reference.md**:
  - `abs_certificate`, both as a 502 and as an `error_code`;
- the summary's `identifiers {books, asin, isbn, by_title,
already_matched}` and a new "Identifiers (match hints)" section;
  - the apply's 409 `import_not_ready` refusal;
- undo, the match dialog's hint order and `identifier_disagrees` from a
hint.
- **data-model.md**: `book_hints` (0074) and "Match hints from an
import". It gives the QueryFor order: own ids win, an Audiobookshelf
hint goes ahead of tag hints, a tag ASIN goes only when the import gave
no identifier, and a tag ISBN may go beside an ABS ASIN. It also covers
ownership and the LibriVox drop: a book placed from LibriVox drops the
hints at its path, and an adopted folder keeps them.
- **web-ui.md** (the abs step and `UnmatchedPrompt` sites),
**scanner.md** (hints don't count as matched), **bridge/librivox.md**,
**cross-repo-contract.md**, and a listening-imports row in the
documentation mapping table.

## Screenshots
`setup/abs.png` is recaptured against #395's server. It shows the inline
Connect form and the Match automatically offer.

## Checks
`npm run build` is green. The docs review (`/simplify` and `/code-review
high`) applied 15 fixes, each checked against the code
(`internal/matchrun/plan.go` QueryFor, `hints_test.go`, the PR1
console).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant