diff --git a/docs-developers/architecture/cross-repo-contract.md b/docs-developers/architecture/cross-repo-contract.md index b5a0006b..ef80817c 100644 --- a/docs-developers/architecture/cross-repo-contract.md +++ b/docs-developers/architecture/cross-repo-contract.md @@ -800,7 +800,9 @@ or `pkg/release` reaches the server through its `go.mod`. The match dialog sends `GET /works/match` the book's tag and path facts: - Its `language` and repeatable `narrator`. An older `metaserve` ignores them. -- Its tag hints, as `asin` and `isbn`. +- Its hints, as `asin` and `isbn`, when it has neither of its own: an Audiobookshelf + import's and its tags' (`matchrun.QueryFor`, in + [this order](../server/data-model.md#match-hints-from-an-import)). Against a `metaserve` that predates the route, the server falls back to `lookup` and `works/search`. The candidates pass `metaserve`'s `score` and `reasons` through to @@ -1632,8 +1634,8 @@ not quiet, deduped per library and project, and in no module. folder. The server does not read it yet, and its scanner skips the name. - The server folds a done result into the book's `book_matches` row ([Matches by work id](#matches-by-work-id)), by `adopted`: a placed book - replaces the path's match and drops its leftover ASIN and ISBN, an adopted one - keeps a match already there. The `bridge_job_finished` event carries + replaces the path's match and drops its leftover ASIN, ISBN and match hints, an + adopted one keeps a match already there. The `bridge_job_finished` event carries `librivox_match` (`matched`, `kept`, `unmatched`). See [Add from LibriVox](../bridge/librivox.md). diff --git a/docs-developers/bridge/librivox.md b/docs-developers/bridge/librivox.md index 6934f2d8..85446547 100644 --- a/docs-developers/bridge/librivox.md +++ b/docs-developers/bridge/librivox.md @@ -168,7 +168,7 @@ book's path depends on the result's `adopted`: | `adopted` | The folder | The fold | |---|---|---| -| `false` | was vacant until this run placed it | replaces the path's `book_matches` row, and drops the path's ASIN and ISBN `book_overrides` (whoever set them) and its `book_enrichment` row, then rebuilds the book's effective metadata. Those rows are a deleted book's leftovers, and `/meta` would ask them before the match. | +| `false` | was vacant until this run placed it | replaces the path's `book_matches` row, and drops the path's ASIN and ISBN `book_overrides` (whoever set them), its `book_enrichment` row and its `book_hints` (an Audiobookshelf import's match hints, which the match dialog would send), then rebuilds the book's effective metadata. Those rows are a deleted book's leftovers, and `/meta` would ask them before the match. | | `true` | is an earlier fetch's book | inserts the match only when the path has none, so an admin's rematch stays, and touches nothing else | | absent (an older Bridge) | not known | replaces the match and touches nothing else | diff --git a/docs-developers/contributing/documentation.md b/docs-developers/contributing/documentation.md index 01c55f6b..76b12123 100644 --- a/docs-developers/contributing/documentation.md +++ b/docs-developers/contributing/documentation.md @@ -116,6 +116,7 @@ mappings: | Generated characters, recaps and descriptions: `.audiosilo/` (the server's `library/attachments.go`, `metadata/attachments.go`, `catalog/attachments.go`, `meta/generated.go`; the Bridge's `internal/generate`, `internal/pipeline/describe_stage.go`), the console's Description part (`describe-section.tsx`) or the player's generate action, mark and About (`src/components/bridge/`, `GeneratedMark`, `aboutContent`) | [bridge/attachments.md](../bridge/attachments.md) + [bridge/generating.md](../bridge/generating.md) + [server/scanner.md](../server/scanner.md#the-bridges-attachments) + the `/meta` [generated content](../server/api/reference.md#generated-content) + [frontend/book-page.md](../frontend/book-page.md#the-primary-action-and-the-action-row) (and its About) + `/users/admin/generating-characters-and-recaps`, `/users/listening/recaps-and-characters`, `/users/listening/book-page` (About) + the `admin/book-description.png` and `web-player/book-about-generated.png` screenshots | | Community matching: `internal/matchrun` (`Classify`, `ReviewFloor`, `tidyTitle`, `workers`, `maxRequestsPerSecond`, `maxFailStreak`), `internal/meta/match.go`, `internal/meta/pacer.go`, a run's waits in `internal/meta/client.go`, the tag hints (`metadata.setHints`, `0067`), the recorded match (`catalog/bookmatches.go`, `book_matches`, `0069`, `0070`, `catalog.matchedExpr`, the book aside's Community card and Unlink, the match dialog's `match`), `health/bulk-match.tsx`, the Add it link (`lib/meta-site.ts`, `add-to-community.tsx`) and the not-matched notice (`unmatched-prompt.tsx`); `works/match`'s params and `expand` in audiosilo-meta | the [reference](../server/api/reference.md#bulk-community-matching) (outcomes, scopes, [what a run asks of the service](../server/api/reference.md#what-a-run-asks-of-the-community-service)) and its [match endpoint](../server/api/reference.md#get-apiv1adminlibrariesidbookmatch) + [configuration.md](../server/configuration.md#community-metadata-metadata) (the waits) + [data-model.md](../server/data-model.md#tag-hints) (and `book_matches` under [durable per-library config](../server/data-model.md#durable-per-library-config-path-keyed-no-fk-to-books)) + [scanner.md](../server/scanner.md) + [web-ui.md](../server/web-ui.md) + [meta/api.md](../meta/api.md#apiv1worksmatch) (and its [rate limit](../meta/api.md#rate-limit-and-caching), which names `workers` and `maxRequestsPerSecond`) + the [cross-repo contract](../architecture/cross-repo-contract.md#13-community-metadata-a-three-repo-seam) (after the workspace `CROSS-REPO.md` §17) + `/users/admin/health#matching-every-book-at-once`, `/users/admin/books#matching-with-community-metadata` (and `#books-matched-to-a-store-listing`, `#unlinking-a-community-match`), `/users/admin/activity` (the coverage wording), `/users/admin/server#community-metadata`, `/users/faq` (what matching sends), `/users/community/meta-site#adding-a-missing-book` (Add it) + the site's `src/pages/metadata.astro` (privacy copy, kept in step with the console's `health.bulkMatch.privacy`) + the `admin/health-bulk-match.png` and `admin/book-community-match.png` screenshots | | The unknown-ASIN report (LibraForge C3): the server's `internal/coverage`, `catalog/coverage.go` (`UnknownASINCandidates`), `meta/report.go`, `unknown_asin_reports` (`0068`), the `unknown_asin_report` server state, `metadata.report_unknown` and `metadata.report_status`, Settings > Metadata's *Help complete the community database* switch; metaserve's intake queue | [configuration.md](../server/configuration.md#the-unknown-asin-report-metadatareport_unknown) + the [reference](../server/api/reference.md#get-apiv1adminsettings) (`metadata.report_unknown`, `metadata.report_status`) + [data-model.md](../server/data-model.md#unknown-asin-reports) + the [cross-repo contract](../architecture/cross-repo-contract.md#13-community-metadata-a-three-repo-seam) + [meta/api.md](../meta/api.md#unknown-asin-intake-queue-optional) and [meta/sync-bot.md](../meta/sync-bot.md#the-reported-asin-lane) + `/users/admin/server#helping-complete-the-community-database`, `/users/admin/books`, `/users/admin/health` (tag hints), `/users/faq` (what is sent) + the `admin/settings-metadata.png` screenshot | +| Listening imports: `internal/importer` (the ABS client and its codes, `abs_certificate` among them; matching, planning, `identifiersFor`), `catalog/imports.go`, the match hints (`catalog/hints.go`, `book_hints`, `0074`, `matchrun.QueryFor`'s order), the console's `features/imports/` (`ImportFlow`, also in the setup flow's abs step) | the [reference](../server/api/reference.md#admin-listening-imports) (its [identifiers](../server/api/reference.md#identifiers-match-hints)) + [data-model.md](../server/data-model.md#listening-imports) (and [match hints](../server/data-model.md#match-hints-from-an-import)) + [web-ui.md](../server/web-ui.md#the-first-run-setup-flow) (the abs step) + `/users/admin/import`, `/users/getting-started/coming-from-audiobookshelf`, `/users/getting-started/first-run` (step 6), `/users/admin/health` (Audiobookshelf's ASINs and ISBNs) + the `admin/settings-import.png` and `setup/abs.png` screenshots | | Metadata mirror mode (`metadata.mode`, `internal/metamirror`, `internal/meta/mirror.go`, the Source card, the mirror panel on Health > System) | [server/configuration.md](../server/configuration.md#mirror-mode-metadatamode-mirror) + the mirror routes in [api/reference.md](../server/api/reference.md#the-local-copys-status-metadatamirror) + `/users/admin/server` (Keeping a local copy), `/users/admin/health` (the local copy), `/users/faq` (privacy) | | `pkg/query`, `pkg/query/querytest`, `pkg/release` in audiosilo-meta, or the artifact schema | [meta/overview.md](../meta/overview.md#public-go-packages) + [meta/data-model.md](../meta/data-model.md#the-compiled-artifact-and-schema-versioning) + the [cross-repo contract](../architecture/cross-repo-contract.md#13-community-metadata-a-three-repo-seam) (mirror-mode servers read the artifact directly) | | Meta schemas, `metaserve` API, or intake tooling (`route.yml`, the intake denylist included) | [meta developer pages](../meta/overview.md) (data model, API, contributing) - and the [cross-repo contract](../architecture/cross-repo-contract.md) when the server's `/meta` envelope is affected, and `/users/community/contributing-from-your-server` when the limits that hold a Bridge's submission change | diff --git a/docs-developers/server/api/reference.md b/docs-developers/server/api/reference.md index 461f0019..1bb31382 100644 --- a/docs-developers/server/api/reference.md +++ b/docs-developers/server/api/reference.md @@ -7155,8 +7155,10 @@ wrong: is the book's author (mostly a composer tag filled with the author, which says nothing of the recording); - `q`, and the ASIN/ISBN: the typed ones, or the book's own when nothing was - typed, or with neither of its own its [tag hints](../data-model.md#tag-hints), - which metaserve looks up itself. + typed, or, with neither of its own, its hints from an Audiobookshelf import and + its [tags](../data-model.md#tag-hints) (`matchrun.QueryFor`, + [the order](../data-model.md#match-hints-from-an-import)). metaserve looks them + up itself. An older metaserve ignores `language` and `narrator`. @@ -7294,8 +7296,8 @@ again. A `503` (over metaserve's match budget) or any other `5xx` from 10% (`meta.PlausibleRuntimeOff`) off the book's length, none of the book's narrators reads it, and another recording of the same work is within 10%. The run then proposes the recording this rule picks among those, `auto` when it has an - ASIN or ISBN (an identifier hit whose length now fits). A tag hint can name - another edition of the right work. + ASIN or ISBN (an identifier hit whose length now fits). A hint, from the tags or + an Audiobookshelf import, can name another edition of the right work. - A narrator reading the named recording keeps it: `auto` within 20%, else held as `length_differs` (see [the outcomes](#bulk-community-matching)). - A plain book never moves onto a dramatization the rule above refuses. @@ -7423,8 +7425,8 @@ Each book's **outcome**: | Outcome | Meaning | |---|---| -| `auto` | Confident (`matchrun.Classify`): the best candidate scores **90** or more, at least **10** ahead of the next, its recording has an ASIN or ISBN (from 2.6.0, one with neither on firmer ground: see below the table), and, when both lengths are known, the recording's runtime is within **10%** of the book's (the closest recording of a work can still be another edition), or, from 2.6.0, within **20%** (`meta.NarratedRuntimeOff`) when the book's narrator reads it (`matchrun.narratorReads`: `reasons.narrator` is `full` and the proposed recording's own credit names one of the book's narrators, `meta.ReadBy`; a placeholder credit such as `Full Cast`, `GraphicAudio`, `Various` or `Unknown` never counts, `meta.PersonNarrator`, as it agrees with every recording carrying it). An identifier hit (`reasons.identifier`: in a match run always a tag hint) needs no lead, unless the next candidate is one too (`review`; a guard, as metaserve names one work per request today). From 2.7.0 the book must also agree with it (`matchrun.identifierAgrees`): `reasons.title`, when judged, at least **0.65** (`IdentifierTitleAgree`), or at least **0.9** (`IdentifierTitleSame`) when `reasons.series` is `conflict`, as the same title at another position is the series numbered another way (a conflict with no title judged is held). Else `review`. So a wrong tag ASIN naming a book with a similar title within 10% of the length is still `auto`, and so, from 2.6.0, is one naming another edition of the right work when a recording of it within 10% takes its place (`meta.FitRecording`, [the dialog's rule](#get-apiv1adminlibrariesidbookmatch)). Applied unless the admin leaves it out. A repick's offers are all `auto`. | -| `review` | A candidate that isn't confident. Applied only if the admin puts it in. One held because its recording's length is more than 10% from the book's, or 20% when the book's narrator reads it (a match run's, as for `auto`, or a refresh's), carries `detail: "length_differs"` (from 2.4.0), worded with the item's `book.duration` and the proposal's `runtime_min`. One whose work holds only dramatizations the book doesn't state it is, none of them the book's by length ([`default_recording_id`](#get-apiv1adminlibrariesidbookmatch), from 2.6.0), carries `detail: "dramatization_only"` (`DetailDramatizationOnly`) and no recording, so nothing is attached unasked. One found by a tag ASIN or ISBN the book doesn't agree with (see `auto`, from 2.7.0) carries `detail: "identifier_disagrees"` (`DetailIdentifierDisagrees`), ahead of `dramatization_only` and `length_differs` when either would also apply. | +| `auto` | Confident (`matchrun.Classify`): the best candidate scores **90** or more, at least **10** ahead of the next, its recording has an ASIN or ISBN (from 2.6.0, one with neither on firmer ground: see below the table), and, when both lengths are known, the recording's runtime is within **10%** of the book's (the closest recording of a work can still be another edition), or, from 2.6.0, within **20%** (`meta.NarratedRuntimeOff`) when the book's narrator reads it (`matchrun.narratorReads`: `reasons.narrator` is `full` and the proposed recording's own credit names one of the book's narrators, `meta.ReadBy`; a placeholder credit such as `Full Cast`, `GraphicAudio`, `Various` or `Unknown` never counts, `meta.PersonNarrator`, as it agrees with every recording carrying it). An identifier hit (`reasons.identifier`: in a match run always a hint, an Audiobookshelf import's or the tags') needs no lead, unless the next candidate is one too (`review`; a guard, as metaserve names one work per request today). From 2.7.0 the book must also agree with it (`matchrun.identifierAgrees`): `reasons.title`, when judged, at least **0.65** (`IdentifierTitleAgree`), or at least **0.9** (`IdentifierTitleSame`) when `reasons.series` is `conflict`, as the same title at another position is the series numbered another way (a conflict with no title judged is held). Else `review`. So a wrong hinted ASIN naming a book with a similar title within 10% of the length is still `auto`, and so, from 2.6.0, is one naming another edition of the right work when a recording of it within 10% takes its place (`meta.FitRecording`, [the dialog's rule](#get-apiv1adminlibrariesidbookmatch)). Applied unless the admin leaves it out. A repick's offers are all `auto`. | +| `review` | A candidate that isn't confident. Applied only if the admin puts it in. One held because its recording's length is more than 10% from the book's, or 20% when the book's narrator reads it (a match run's, as for `auto`, or a refresh's), carries `detail: "length_differs"` (from 2.4.0), worded with the item's `book.duration` and the proposal's `runtime_min`. One whose work holds only dramatizations the book doesn't state it is, none of them the book's by length ([`default_recording_id`](#get-apiv1adminlibrariesidbookmatch), from 2.6.0), carries `detail: "dramatization_only"` (`DetailDramatizationOnly`) and no recording, so nothing is attached unasked. One found by a hinted ASIN or ISBN (its tags' or an Audiobookshelf import's) the book doesn't agree with (see `auto`, from 2.7.0) carries `detail: "identifier_disagrees"` (`DetailIdentifierDisagrees`), ahead of `dramatization_only` and `length_differs` when either would also apply. | | `none` | No candidate; or, in a match run, a best candidate scoring under **60** (`matchrun.ReviewFloor`) on metaserve's scale, with `detail: "weak_match"` (`DetailWeakMatch`), its `score` kept for the review's wording and its `proposal` empty (from 2.6.0). A candidate without `reasons` (an older metaserve's fallback search, on another scale) stays `review`. The match dialog has no such floor: it lists its candidates (up to 6) however low they scored. | | `error` | The service failed for this book (`detail: "metadata_unavailable"`), from 2.7.0 also when the best candidate's work can't be read for any reason but a `404` (a gone work gives way to the next). [Ten in a row](#what-a-run-asks-of-the-community-service) stop the run. | @@ -8627,8 +8629,9 @@ the person's access. The console's **Server > Settings > Import**: another server's listening history copied into a person's own, so someone moving to AudioSilo keeps their -stats. v1 reads **Audiobookshelf** (ABS) only and is **admin only**: no -capability flag, no player route (the console ships inside the server). The +stats. It also keeps Audiobookshelf's ASIN and ISBN for the books here as +[match hints](../data-model.md#match-hints-from-an-import) (`book_hints`). v1 +reads **Audiobookshelf** (ABS) only and is **admin only**: no capability flag, no player route (the console ships inside the server). The work is `internal/importer` (the ABS client, matching, planning, the background fetch); the rows are `catalog/imports.go`; the handlers (`api/handlers_import.go`) are transport only. What an applied import writes @@ -8645,7 +8648,9 @@ the server stopped mid-way can't resume (it fails as `interrupted`). The ABS client is read-only (`GET`s), checks `GET /status` says `audiobookshelf` (and `isInit`) **before** sending the token anywhere, accepts only `http`/`https` addresses without credentials, a query or a fragment, follows redirects only to -the same host (never `https` → `http`, at most 4 of them), caps every response size +the same host (never `https` → `http`, at most 4 of them), trusts only the +certificates the system trusts (a self-signed one fails as `abs_certificate`, and +there is no setting to skip the check), caps every response size and times out each request (3 minutes) and the whole fetch (30 minutes). Its errors are fixed English sentences with a code; an ABS response body never reaches a response or a log. There is no private-address block: ABS usually @@ -8686,7 +8691,8 @@ An **import** reads: "finished": 161, "bookmarks": 73, "first_listen": "2021-02-03T20:11:09Z", - "last_listen": "2026-03-13T22:40:51Z" + "last_listen": "2026-03-13T22:40:51Z", + "identifiers": { "books": 640, "asin": 612, "isbn": 41, "by_title": 22, "already_matched": 95 } } } ``` @@ -8698,7 +8704,7 @@ An **import** reads: | `cutoff` | sessions **starting at or after** it are skipped; RFC 3339 UTC to the second (rounded down); `null` = none. Always the resolved value, never `"auto"` | | `cutoff_utc_offset` | the server's offset from UTC at the cutoff, in minutes (that date's, daylight saving included); `null` without a cutoff. A client shows and edits the cutoff in server time with it (a chosen day is the server's midnight), whatever its own zone | | `created_at`, `applied_at` | RFC 3339 UTC; `applied_at` `null` until applied | -| `error`, `error_code` | `""` unless `failed`. `error` is a safe English sentence; `error_code` is `abs_unreachable` (also a timeout), `abs_unauthorized`, `not_abs`, `fetch_failed` or `interrupted` | +| `error`, `error_code` | `""` unless `failed`. `error` is a safe English sentence; `error_code` is `abs_unreachable` (also a timeout), `abs_certificate`, `abs_unauthorized`, `not_abs`, `fetch_failed` or `interrupted` | | `summary` | `null` while fetching or when the fetch failed; planned at review (recounted by a cutoff change) and rewritten at apply with what was written | The fetched history is kept (gzipped, in `import_payloads`) only while the @@ -8721,7 +8727,14 @@ or deleted. - `finished`: the books given a finish (or a missing finish date) from ABS; - `bookmarks`: the bookmarks added; - `first_listen` / `last_listen`: the earliest and latest imported session start - (`null` without sessions). + (`null` without sessions); +- `identifiers`: the books here the import gives Audiobookshelf's ASIN or ISBN as + a match hint (see [Identifiers](#identifiers-match-hints) below). `books` counts + them, `asin` and `isbn` the books given each, `by_title` those paired by title + and author rather than by path, and `already_matched` the paired books left out + because they are matched here already. Absent from a summary planned before the + server carried identifiers, and all zero once such an import is planned again, + as its stored fetch holds none. A client treats a missing `identifiers` as none. `GET /admin/imports/{id}` adds the books that will be skipped, most listened first, at most 500: @@ -8766,6 +8779,31 @@ deleted items that resolve to one book all match it, with at most one item still in ABS, and their sessions merge. The apply matches again, so a book added since the review is found. +#### Identifiers (match hints) + +The fetch also keeps every ABS book with a valid ASIN or ISBN in the libraries the +token sees (`Payload.Identified`, JSON `identified`, whoever's history it is in). +Review and apply pair them with books here (`importer.identifiersFor`): + +- among **every** book here, whatever the target user can access, as the hints are + the server's, not the person's; +- by the **path** tier, then the **title and author** tier, never by ASIN or ISBN + (those are what is being carried). A title pairing is guarded later by the match + run's agreement check (`matchrun.identifierAgrees`); +- leaving out a book two identified ABS books pair with (contested), an ABS folder + this server splits into disc books, and a book already matched here + (`catalog.matchedExpr`: its own ASIN or ISBN, or a recorded match), which is + counted as `already_matched`. + +The imports of one fetch carry the same identifiers, whichever person each is +for. An apply writes them to `book_hints` (source `abs`, `import_id` the import), +replacing each book's hint whole, so the import applied last owns a book's hint +([ownership](../data-model.md#match-hints-from-an-import)). They never become `books.asin` or `isbn` and never reach the +player wire. A hinted book stays unmatched until a match run's apply or the match +dialog writes a match, which is why the console offers **Match automatically** once +an import is applied. How a match sends them is +[`QueryFor`'s rule](../data-model.md#match-hints-from-an-import). + **Cutoff** values, in a start or a `PATCH`: `"auto"` (each user's first listening recorded here, `catalog.ListeningStart`: their earliest non-imported session or non-estimated rolled-up day; `null` when they have none), `null` @@ -8800,7 +8838,7 @@ with the same username (any case), if any. |---|---| | `200` | the server's version and its users | | `400` | `invalid request`; `code: "invalid_url"` - not an absolute `http`/`https` URL, or it carries credentials, a query or a fragment; `code: "invalid_import"` - the token is empty, over 8 KiB or holds a line break or NUL (ABS is not contacted) | -| `502` | `code: "abs_unreachable"` (no answer, a timeout or a `5xx`), `"not_abs"` (`/status` didn't answer as an initialised Audiobookshelf), `"abs_unauthorized"` (the token was refused) or `"fetch_failed"` (an unreadable answer) | +| `502` | `code: "abs_unreachable"` (no answer, a timeout or a `5xx`), `"abs_certificate"` (the `https` handshake refused ABS's certificate: untrusted, often self-signed, for another name, or expired; the console points at ABS's plain `http://` address), `"not_abs"` (`/status` didn't answer as an initialised Audiobookshelf), `"abs_unauthorized"` (the token was refused) or `"fetch_failed"` (an unreadable answer) | Not audited (it changes nothing). @@ -8865,9 +8903,18 @@ Applies a `review` import. The import moves to `applying`, then one transaction: `undone`, so a re-import never double counts; 2. matches and plans again against the user's state; 3. writes it: sessions, a `listening_history` span per session, estimates, - progress and bookmarks; + progress, bookmarks and the [match hints](#identifiers-match-hints); 4. marks it `applied`. +An import that writes no listening for the person (identifiers at most) applies when +the person has no applied import from the source. When they have one, step 1 would +take it back and replace it with nothing, so the apply is refused +(`catalog.ErrImportNoListening`), the transaction rolls back and the previous import +stays applied. Listening is a session, a rolled-up day, a bookmark, or a progress row +the import creates or moves on (position, finish or finish date); a row whose only +change is an earlier start date is not. The console turns **Apply** off for that +case (`wouldOnlyUndo`). Undo the applied import first to replace it. + A session already older than the session retention (`activity.session_days` as set when the apply runs) is written as the `listening_daily` totals the daily prune would make of it, rather than as a session. So it shows in the stats and @@ -8881,14 +8928,16 @@ Audible. The request's cancellation doesn't stop it. A failure puts it back in | Status | Meaning | |---|---| | `404` | `code: "import_not_found"` | -| `409` | `code: "import_not_ready"` - it isn't in `review` | +| `409` | `code: "import_not_ready"` - it isn't in `review`, or it writes no listening for the person while they have an applied import from the source (the message says to undo that one first) | ### `POST /api/v1/admin/imports/{id}/undo` Takes an applied import back out. Its sessions, rolled-up days, estimates, -history spans and bookmarks are deleted by `import_id`; a bookmark the person -edited or labelled since is kept and becomes theirs. Each progress row it changed -is restored to what it was before, unless it changed since through a player's save +history spans, bookmarks and match hints are deleted by `import_id` (a hint +belongs to the import that wrote it last, see +[ownership](../data-model.md#match-hints-from-an-import)). Matches already applied +stay. A bookmark the person edited or labelled since is kept and becomes theirs. +Each progress row it changed is restored to what it was before, unless it changed since through a player's save or an edit, in which case it stays. Answers `200` with the import (`undone`). Audited as `import.undo` (`import`). A row restored from one an Audible account wrote comes back under **no** device (`progressPrior.restored`), never as diff --git a/docs-developers/server/data-model.md b/docs-developers/server/data-model.md index 262d3b01..0e107003 100644 --- a/docs-developers/server/data-model.md +++ b/docs-developers/server/data-model.md @@ -25,7 +25,7 @@ to understand before you change the schema. `collection_items` and `ratings`; - per-library config: `folder_overrides`, `book_enrichment`, `issue_ignores`, `book_overrides`, `chapter_overrides`, `chapter_choices`, - `book_covers` and `book_matches`. + `book_covers`, `book_matches` and `book_hints`. The rest of a listener's own state hangs off the account rather than a path: `collections`, `collection_shares`, `listening_goals` and @@ -110,6 +110,7 @@ erDiagram libraries ||--o{ chapter_overrides : "durable config" libraries ||--o{ book_covers : "durable config" libraries ||--o{ book_matches : "durable config" + libraries ||--o{ book_hints : "durable config" libraries ||--o{ issue_ignores : "durable config" libraries ||--o{ scan_runs : "scan history" libraries ||--o{ community_chapters : "path-keyed, NO FK to books" @@ -1178,8 +1179,9 @@ else. `(user_id, id)`. - **`import_payloads`**: PK `import_id` (FK CASCADE). It holds the fetched history as gzipped JSON: the user's book items, sessions, progress and - bookmarks, and nothing secret. A cutoff change and the apply therefore never - fetch again. It is a table of its own so that reading `imports` never walks a + bookmarks, and every ABS book with an ASIN or ISBN (`identified`, for the + [match hints](#match-hints-from-an-import)). Nothing secret. A cutoff change + and the apply therefore never fetch again. It is a table of its own so that reading `imports` never walks a large blob's overflow pages. The row is deleted once the import is applied, undone or deleted. - **`import_id`** on `listening_sessions`, `listening_daily`, @@ -1254,6 +1256,14 @@ import from the same source. the two, and a finished row with no finish date takes ABS's. - A changed row's `version` goes up by one, and its before and after go in `import_progress_prior`. +- **Match hints.** A `book_hints` row (source `abs`, `import_id` the import) for + each book here the identified ABS books pair with (see + [Match hints from an import](#match-hints-from-an-import)). + +An apply that writes no listening is refused (`catalog.ErrImportNoListening`) +when the person has a previous applied import from the source, so that one stays +applied (see the +[apply route](api/reference.md#post-apiv1adminimportsidapply) for what counts). The cutoff gates sessions and estimates only. Its default (`"auto"`) is the person's first listening recorded here (`catalog.ListeningStart`): their @@ -1264,8 +1274,8 @@ counted once. #### Undo, retention and restarts **Undo**, and an apply replacing the previous import, deletes the import's -sessions, daily rows, history spans and untouched bookmarks by `import_id`. It -restores each `import_progress_prior` row whose progress is still exactly as the +sessions, daily rows, history spans, untouched bookmarks and `book_hints` rows by +`import_id`. It restores each `import_progress_prior` row whose progress is still exactly as the import wrote it (`sameProgress`: path, version, `updated_at`, position, finish and dates). A restored row gets a version above the import's. @@ -1801,6 +1811,10 @@ keeps the report to once a day across restarts. The run status is the backup. - **`book_matches`** *(0069; `offered_asin` and `offered_isbn` in 0070)*: a book's recorded community match, described after this list. +- **`book_hints`** *(0074)*: PK `(library_id, path, source)`, `asin`, `isbn`, + `import_id`, `set_at`. Identifiers another app gave a book, kept as hints for a + community match only. See + [Match hints from an import](#match-hints-from-an-import). - **`issue_ignores`** *(0017)*: PK `(library_id, path, kind)`, `created_by` (FK to `users`, `ON DELETE SET NULL`), `created_at`. An admin's "ignore this" on a Health issue. `kind` is one of `catalog.IssueKinds` (`scan_error`, @@ -1842,7 +1856,8 @@ Two things write it: - the fold of a done `librivox.fetch` (`catalog.FoldLibrivoxFetch`), for the book the Bridge placed: `how` `accepted`, `set_by` the admin who asked, no run and no offered identifiers. By the result's `adopted`, it replaces a row already at - that path (a placed book, which also drops the path's leftover ASIN and ISBN), + that path (a placed book, which also drops the path's leftover ASIN, ISBN and + `book_hints`), keeps it (an adopted book), or replaces it and drops nothing (no `adopted`, an older Bridge). It touches no other path (see [Add from LibriVox](../bridge/librivox.md#the-books-identity)). @@ -2018,8 +2033,10 @@ never on the wire. Only `GetBook` and a run's subjects read them, and a tagged book stays `unmatched`. `matchrun.QueryFor` sends a hint as `BookASIN` or `BookISBN` when the book has -neither of its own. A hint the community database knows is then confident when -its length fits and the book agrees with its title (see +neither of its own, after an import's (see +[Match hints from an import](#match-hints-from-an-import) for the order). A hint +the community database knows is then confident when its length fits and the book +agrees with its title (see [Bulk community matching](api/reference.md#bulk-community-matching)). It becomes the book's ASIN only by an apply. A match run may propose another recording of the hint's work that fits the book's length better @@ -2037,6 +2054,53 @@ it stays an identifier. These are scanned values, like `released`, and every index rewrites them. +### Match hints from an import + +`book_hints` (migration 0074) holds the ASIN and ISBN another app gave a book. +Today the only source is `abs`. An applied +[Audiobookshelf import](#listening-imports) writes one row for each book here that +an ABS book with an identifier pairs with (`importer.identifiersFor`, by path, then +by title and author, across every book; see +[Identifiers](api/reference.md#identifiers-match-hints)). Like a tag hint, it is a +hint for a community match only. It is never `books.asin` or `isbn`, never on the +wire, and a hinted book stays `unmatched` until a match run's apply or the match +dialog writes its match. Only `GetBook` (a sub-select) and a run's subjects (a +`LEFT JOIN`, `matchSubjectJoin`) read it, as `Book.HintASIN` and `HintISBN` +(`json:"-"`). + +**What a match sends** (`matchrun.QueryFor`, for the match dialog and a run alike): + +1. A book with an ASIN or ISBN of its own sends only its own ASIN and ISBN, and + no hint. +2. Otherwise, with an import ISBN, it sends the import's ASIN and ISBN and nothing + from its tags. +3. Otherwise it sends the import's ASIN, else the tag's ASIN, and beside it the + tag's ISBN. + +So the import's identifiers go ahead of the tags'. A tag ASIN is sent only when the +import gave no identifier. A tag ISBN may go beside an import's ASIN, because +metaserve asks the ASIN first and the ISBN only when it doesn't know that one, so +the tag's ISBN is only a fallback. A tag ASIN beside an import ISBN would be asked +first, ahead of the import, so it never goes. A hit is judged as a tag hint's is +(`Classify`, `identifierAgrees`). The unknown-ASIN report sends a book's tag ASIN +only, never its import hint (`catalog.UnknownASINCandidates`). + +**Ownership.** An apply replaces each book's `abs` row whole, with its own +`import_id`, so the import applied last owns a book's hint. An undo, or an apply +replacing the person's previous import, deletes that import's rows by `import_id` +(index `idx_book_hints_import`). A row a later import took over stays with it, and +a row this import took over from an earlier one goes, even while the earlier import +is still applied. `import_id` has no foreign key, so the row outlives the +import's record, as `book_matches.run_id` does. + +**Durability.** The table is path-keyed with no FK to `books`, so a rebuild keeps +it. A move carries it (`moveBookState`). A disc join copies the first part's row +from each source. Deleting the library drops it (`ON DELETE CASCADE`). A book +the Bridge places from LibriVox drops the row at its path with the path's other +leftover identifiers (`catalog.FoldLibrivoxFetch`, see +[Add from LibriVox](../bridge/librivox.md#the-books-identity)). An adopted +folder keeps it, as does an older Bridge's fold (no `adopted`). + ### Labels: genres and tags `genres` and `tags` (`catalog/labels.go`) are override fields like any other, @@ -2166,6 +2230,7 @@ The migration history so far: | 0070 | `book_match_offered` | `book_matches.offered_asin` and `offered_isbn` (`TEXT NOT NULL DEFAULT ''`): the identifiers the [recorded community match](#durable-per-library-config-path-keyed-no-fk-to-books) offered. `match_run_items.unchanged` (`TEXT NOT NULL DEFAULT ''`): the comma-separated scopes under which a refresh item had nothing to change when recorded; an apply under one leaves it out without reading the book again. No backfill: `''` behaves as before. No data moves | | 0071 | `audible_sync_followups` | The `audible_title_match_update` trigger, recreated so a `preorder` title is left out of the sync and its release re-offers the book's place; `audible_accounts.refusal_ended_at` (see [The Bridge](#the-bridge)). No data moves | | 0073 | `podcast_episode_indexed` | `podcast_episodes.indexed_at`, backfilled from each episode's `books.indexed_at` (see [Podcasts](#podcasts)) | +| 0074 | `book_hints` | [Match hints from an import](#match-hints-from-an-import): `book_hints` (`library_id` `ON DELETE CASCADE`, `path`, `source`, `asin` and `isbn` `TEXT NOT NULL DEFAULT ''`, `import_id` `INTEGER NOT NULL DEFAULT 0`, `set_at`; primary key `(library_id, path, source)`) and the index `idx_book_hints_import` on `import_id`. No data moves | ## SQLite choices diff --git a/docs-developers/server/scanner.md b/docs-developers/server/scanner.md index 4b841118..3c830da2 100644 --- a/docs-developers/server/scanner.md +++ b/docs-developers/server/scanner.md @@ -1770,7 +1770,7 @@ predicate over `books b`, shared by the counts (`IssueCounts`) and the lists | `suspect` | `suspect_parts >= 2`, and the folder has no `book` override | | `split_discs` | it is the **first** disc (by `rel_path`) of a book split across disc folders. `split_parent` is set, and that folder has no override of either mode (either one settles how it reads). Its fix sets `book` on `split_parent`, which joins the discs | | `no_cover` | a scan has checked it (`has_cover` not `NULL`), and it has no sidecar image, no embedded art and no custom cover | -| `unmatched` | it has no ASIN, no ISBN and no recorded community match (`book_matches`, see [Data model](data-model.md#durable-per-library-config-path-keyed-no-fk-to-books); `NOT catalog.matchedExpr`). [Tag hints](data-model.md#tag-hints) don't count. Offered only while community metadata is on | +| `unmatched` | it has no ASIN, no ISBN and no recorded community match (`book_matches`, see [Data model](data-model.md#durable-per-library-config-path-keyed-no-fk-to-books); `NOT catalog.matchedExpr`). [Tag hints](data-model.md#tag-hints) and an import's [match hints](data-model.md#match-hints-from-an-import) don't count. Offered only while community metadata is on | | `unknown_asin` | its own ASIN is one the [unknown-ASIN report](configuration.md#the-unknown-asin-report-metadatareport_unknown) found the community database doesn't know (an `unknown_asin_reports` row with `unknown = 1`, matched on the upper-cased, trimmed ASIN; `unknownASINExpr`), and it has no ISBN, no `book_matches` row and no ASIN a community match gave it (`book_overrides` of source `community`), so matching it by hand settles it. It still counts as matched (`matchedExpr`), so Match automatically skips it. The summary lists the kind only while the report is on, since only its lookups fill the table | | `series_spellings` | another book of its library spells its main series another way: the same `series_key`, a different `series`. This takes two range seeks on `idx_books_series_key`, for a spelling below its own or above. `SeriesSpellingGroups` lists them per series from the same predicate, and the console's fix sets one spelling with a bulk edit | | `no_chapters` | it is longer than **2 hours** and has at most one chapter. The console shows why community chapters weren't used from the row's `chapters_check` (a failed [community chapter check](community-chapters.md)) | diff --git a/docs-developers/server/web-ui.md b/docs-developers/server/web-ui.md index 92b235a8..51a24f94 100644 --- a/docs-developers/server/web-ui.md +++ b/docs-developers/server/web-ui.md @@ -524,8 +524,10 @@ The Overview is built on `GET /admin/stats`, `GET /admin/sessions/live`, `features/health/unmatched-prompt.tsx`): the `unmatched` category's count, ignored books left out, with **Match automatically** opening Health's Not matched list. It shows only while that count is above zero, so never while - community metadata is off. Library > Books shows the same notice while - browsing. + community metadata is off. It never starts a run. Library > Books shows the + same notice while browsing, Settings > Import's Past imports card once an import + is applied, and the setup flow's abs step (there with `newTab`, so Health opens + in a new tab and the flow stays open). - **Listening now** (`GET /admin/sessions/live`) has one card per live session, playing first, each linking to Activity > Live now. A card's second line is the chapter label described under [Activity > Live now](#activity--live-now), else the author or device. @@ -1906,7 +1908,7 @@ outside `AppShell`. `Root` signs the admin in, then `SignedIn` renders the bare shows with the setup state loaded decides, once (`redirectDecision` in `setup-model.ts`). The Overview, where a plain visit to `/admin` lands, goes to the flow. Any other page stays: a bookmark, a reload, the flow itself, or a link -the flow opens in a new tab (the ABS step's **Open the import**, the Reach +the flow opens in a new tab (the ABS step's **Match automatically**, the Reach step's HTTPS settings), which starts a session of its own. Either way the session is marked, so later pages in it are never redirected: **Finish later** (a link to Overview) and moving on from a deep link stay where they go. @@ -2018,9 +2020,18 @@ bridge phone` (`SETUP_STEPS`). Each step has Skip and, after the first, Back (`addressMismatch`). - The TLS mode, with what it means for phones, plus `tls_home_http` in self-signed mode. These are restart settings, badged. -6. **abs.** Points to Server > Settings > Import (in a new tab) and says that - ABS `metadata.json` files are read. An info-only step: Continue records - `done`, Skip `skipped`. +6. **abs.** The listening import itself, inline: Settings > Import's + `ImportFlow` (`features/imports/import-topic.tsx`) with `embedded` (no step + counter). It also says that ABS `metadata.json` files are read. + - While a books library is scanning or waiting to (`booksScanning` over the + polled libraries, no wire field), `blocked` turns Connect off with "Wait for + the scan to finish", because the import pairs ABS's books with the ones the + scan finds. Only Connect waits; the fields, Continue and Skip don't. + - `offerMatch` shows the `UnmatchedPrompt` once the server has a books library + and none is scanning, before any import is applied. After an apply, the + Past imports card shows it instead (one render site). Either way it opens + Health in a new tab. + - Continue records `done` whatever was imported, Skip `skipped`. 7. **backups.** `backups.schedule` and `keep` (1-365), the backup folder shown read-only, and `general.update_check`. `backups.dir` is restart-only and read-only in the settings API. diff --git a/docs-users/admin/books.md b/docs-users/admin/books.md index 0b22923c..43b85719 100644 --- a/docs-users/admin/books.md +++ b/docs-users/admin/books.md @@ -84,7 +84,9 @@ with community metadata** or run [Match automatically](health.md#matching-every- - the book's tagged title, author, series, narrators and language, and its length; -- any ASIN or ISBN, and what you type; +- its ASIN or ISBN, else one from its tags or an + [Audiobookshelf import](import.md#match-your-books-after-an-import), and what + you type; - up to three names from its path (the author, series and book folders), never the full path. diff --git a/docs-users/admin/health.md b/docs-users/admin/health.md index d9f57d05..284faa8c 100644 --- a/docs-users/admin/health.md +++ b/docs-users/admin/health.md @@ -95,8 +95,8 @@ was deleted." Once the folder is back, **Retry** scans it. See **Not matched to community metadata** has a **Match automatically** card above its list. It looks up every book in the list in one pass. Nothing changes until you review and apply what it found. While there are unmatched books, the -overview and **Library > Books** show how many, with a **Match automatically** -button that opens this list. +overview, **Library > Books** and an applied import's **Past imports** show how +many, with a **Match automatically** button that opens this list. 1. Pick a library and click **Find matches**. You can leave the page while it runs. @@ -150,9 +150,18 @@ If you don't trust where your tags came from, check **Confident** before you apply. Correct the tag, or match the book by hand, to stop it coming back. ::: -An ASIN from an Audiobookshelf `metadata.json` counts as the book's own straight -away (see -[Audiobookshelf's metadata.json files](../getting-started/organizing-your-library.md#coming-from-audiobookshelf)). +### Audiobookshelf's ASINs and ISBNs + +Audiobookshelf's identifiers reach AudioSilo in two ways, and they count +differently: + +- **From an [Audiobookshelf import](import.md#match-your-books-after-an-import).** + They are match hints, like tags. A match looks them up before the book's tags, + and the book stays **Not matched** until a match applies. A wrong one can look + confident too, and correcting the tag doesn't stop it: match the book by hand. +- **From a `metadata.json` file beside the book.** The ASIN or ISBN counts as the + book's own straight away, so the book is matched (see + [Audiobookshelf's metadata.json files](../getting-started/organizing-your-library.md#coming-from-audiobookshelf)). ### Store listings with no ASIN or ISBN diff --git a/docs-users/admin/import.md b/docs-users/admin/import.md index 4683320e..86d26ea6 100644 --- a/docs-users/admin/import.md +++ b/docs-users/admin/import.md @@ -1,11 +1,14 @@ --- title: "Import your Audiobookshelf history" -description: "Moving from Audiobookshelf? Copy each person's listening history, progress and bookmarks into AudioSilo, check it, and undo it if needed." +description: "Moving from Audiobookshelf? Copy each person's listening history, progress and bookmarks into AudioSilo, bring its ASINs and ISBNs for matching, check it, and undo it if needed." --- Moving from Audiobookshelf? **Server > Settings > Import** in the [admin console](console-tour.md) copies each person's listening history over, so their stats, streaks and progress carry on instead of starting at zero. +It also brings Audiobookshelf's ASIN and ISBN for your books, so they can be +matched to community metadata. The setup flow runs the same import in its +[Coming from Audiobookshelf step](../getting-started/first-run.md#step-6-coming-from-audiobookshelf). You need to be an admin. AudioSilo only reads from Audiobookshelf and changes nothing there. You check each import before anything is written, and you can @@ -19,6 +22,8 @@ undo it. - **How far they got in each book**, and when they started and finished it. Progress only ever moves forward: an import never rewinds a book. - **Bookmarks**, skipping ones they already have here. +- **Audiobookshelf's ASIN and ISBN** for each book it has them for, as match + hints. See [Match your books after an import](#match-your-books-after-an-import). What doesn't come over: podcasts, collections and playlists, accounts and passwords, and books that aren't in your AudioSilo libraries. @@ -36,7 +41,11 @@ passwords, and books that aren't in your AudioSilo libraries. - **The Audiobookshelf address, as the AudioSilo server sees it.** The server fetches the history itself, so the address must work from the machine AudioSilo runs on. If both run in the same Docker Compose project, use the - service name, for example `http://audiobookshelf`. + service name, for example `http://audiobookshelf`. Use the plain `http://` + address on your network if Audiobookshelf's `https://` one has a self-signed + certificate (see [When a fetch fails](#when-a-fetch-fails)). +- **A finished scan.** The import pairs Audiobookshelf's books with the books + AudioSilo has found, so let a new library's first scan finish. ## Import the history @@ -51,7 +60,8 @@ passwords, and books that aren't in your AudioSilo libraries. 5. Press **Fetch history for 1 person** (or *n* people). Nothing is written yet. A long history can take a minute, and you can leave the page. 6. Under **Imports to review**, check each person's figures: hours, sessions, - books matched and books not matched. + books matched, books not matched, and books given an **ASIN or ISBN for + matching**. 7. Press **Apply for *person*** and confirm. The import then moves to **Past imports**. @@ -76,12 +86,44 @@ then by title and author. The review lists each skipped book with the reason: Then import that person again. A new import replaces the previous one, so nothing is counted twice. +## Match your books after an import + +An import doesn't match your books. It brings Audiobookshelf's ASIN and ISBN for +every book Audiobookshelf has them for in the libraries the token can see, +whoever listened to it. A match run then looks them up in the community +database. + +- The server pairs these books with yours by folder path, then by title and + author. +- It leaves out a book two Audiobookshelf books pair with, and a book that is + already matched here. +- The identifiers are **match hints**. They never become the book's ASIN or + ISBN and never show in the apps. +- A match looks Audiobookshelf's identifiers up ahead of any in the book's + tags. + +So nothing shows until a match run applies. Once an import is applied, with +community metadata on, **Past imports** says how many books are not matched, +with a **Match automatically** button. It opens the unmatched books in Library health, where you start the run, +review what it found and apply it. See +[Matching every book at once](health.md#matching-every-book-at-once). + +An import with no listening for its person still brings the identifiers, and +you can apply it for those alone. It can't replace an import already applied +for that person, because applying it would undo their history and add none. +**Apply** stays off and the review says so. Undo the applied one first if you +want to replace it. + ## Undoing an import **Past imports** lists every import. **Undo** removes the sessions and bookmarks it added and puts the person's progress back. Anything the person changed since, such as listening further, is kept. +Undo also removes the match hints that import brought. Each book keeps the hint +of the import applied last, so undoing the latest import removes its hints even +where an earlier import carried the same ones. Matches you already applied stay. + A person's **Listening** tab also lists their imports, with **Undo**. See [A person's page](users-and-invites.md#listening). @@ -94,9 +136,16 @@ Activity > Sessions, they show "Imported from Audiobookshelf". ## When a fetch fails -The import card says why. Check that Audiobookshelf is running, that its -address works from the AudioSilo server, and that the token is right (use an -admin or root account's token to import other people). Then **Discard** it and +**Connect** checks the address and token first and says what's wrong under the +field. If it says Audiobookshelf's certificate isn't trusted, its `https://` +address uses a certificate the AudioSilo server doesn't trust, usually a +self-signed one. Use its plain `http://` address on your network instead, for +example `http://192.168.1.20:13378`. + +For a fetch that fails later, the import card says why. Check that +Audiobookshelf is running, that its address works from the AudioSilo server, and +that the token is right (use an admin or root account's token to import other +people). Then **Discard** it and start again. Nothing was written. ## Good to know diff --git a/docs-users/features.md b/docs-users/features.md index 69658dab..2ecb97f0 100644 --- a/docs-users/features.md +++ b/docs-users/features.md @@ -66,7 +66,7 @@ Everything else works with the server alone. - [Back up your Audible books](admin/audible.md) and keep your places in step with Audible. **(Bridge)** - [Add free books from LibriVox](admin/librivox.md): search LibriVox and download a public-domain audiobook into a library, already matched. **(Bridge)** - [Contribute from your server](community/contributing-from-your-server.md): share corrections and recaps with the community. **(Bridge)** -- [Import from Audiobookshelf](admin/import.md): bring over listening history, progress and bookmarks. +- [Import from Audiobookshelf](admin/import.md): bring over listening history, progress and bookmarks, and Audiobookshelf's ASINs and ISBNs for matching. ## Running the server diff --git a/docs-users/getting-started/coming-from-audiobookshelf.md b/docs-users/getting-started/coming-from-audiobookshelf.md index 5694ad70..8c59501d 100644 --- a/docs-users/getting-started/coming-from-audiobookshelf.md +++ b/docs-users/getting-started/coming-from-audiobookshelf.md @@ -23,11 +23,13 @@ A book with a `metadata.json` file then shows its Audiobookshelf details in Audi ## Bring listening history across -**Server > Settings > Import** copies each person's listening sessions, progress and bookmarks from Audiobookshelf. [Invite](../admin/users-and-invites.md#inviting-someone) everyone first, so each person has an account to import into. See [Import your Audiobookshelf history](../admin/import.md). +**Server > Settings > Import** copies each person's listening sessions, progress and bookmarks from Audiobookshelf. It also brings Audiobookshelf's ASIN and ISBN for your books, as hints for matching. The setup flow's **Coming from Audiobookshelf?** step runs the same import. + +[Invite](../admin/users-and-invites.md#inviting-someone) everyone first, so each person has an account to import into. Give the import Audiobookshelf's plain `http://` address on your network if its `https://` one has a self-signed certificate (see [When a fetch fails](../admin/import.md#when-a-fetch-fails)). See [Import your Audiobookshelf history](../admin/import.md). ## Match your books -A scan matches no book by itself, apart from books whose `metadata.json` names an ASIN or ISBN. An ASIN in a book's tags is only a hint for the next step. To match the rest: +A scan matches no book by itself, apart from books whose `metadata.json` names an ASIN or ISBN. An ASIN in a book's tags, or one the import brought, is only a hint for the next step. After an import, its **Past imports** list has a **Match automatically** button that opens the list in step 1. To match the rest: 1. Open **Health** in the admin console and select **Not matched to community metadata**. This is the list of your unmatched books. 2. On its **Match automatically** card, pick the library and select **Find matches**. diff --git a/docs-users/getting-started/first-run.md b/docs-users/getting-started/first-run.md index bbee5de5..3f21f2e0 100644 --- a/docs-users/getting-started/first-run.md +++ b/docs-users/getting-started/first-run.md @@ -151,7 +151,13 @@ See [Reach your server from outside your home](./remote-access.md). ![Step 6: the Audiobookshelf import](/img/screenshots/setup/abs.png) -**Open the import** brings each person's listening progress across from Audiobookshelf (see [the import](../admin/import.md)), in a new tab. AudioSilo also reads the `metadata.json` files Audiobookshelf leaves beside your books. [Move over from Audiobookshelf](./coming-from-audiobookshelf.md) covers the rest. +This step runs the [Audiobookshelf import](../admin/import.md) right here. Connect to Audiobookshelf, review what matches for each person, then apply it. It brings each person's listening progress across, and Audiobookshelf's ASIN and ISBN for your books as hints for matching. + +- **Wait for the scan to finish.** The import pairs Audiobookshelf's books with the ones the scan finds, so **Connect** stays off while the library's first scan runs. +- **Then match your books.** If community metadata is on and books are not matched, the step says how many, with **Match automatically**. It opens Library health in a new tab, where you [start the run](../admin/health.md#matching-every-book-at-once) and review what it found. Nothing is matched until you apply it. +- **Continue** moves on whether or not you imported anything. The import stays in **Server > Settings > Import**. + +AudioSilo also reads the `metadata.json` files Audiobookshelf leaves beside your books. [Move over from Audiobookshelf](./coming-from-audiobookshelf.md) covers the rest. ### Step 7: Backups and updates diff --git a/screenshots/capture-setup.mjs b/screenshots/capture-setup.mjs index f9b9a4ab..c2dc8f7f 100644 --- a/screenshots/capture-setup.mjs +++ b/screenshots/capture-setup.mjs @@ -160,8 +160,14 @@ await step('the console flow', async () => { await tall('setup/reach.png'); await leave('Continue', 'Coming from Audiobookshelf?'); - // 6. Audiobookshelf: an info-only step, so Continue records it done. - await shoot(page, 'setup/abs.png'); + // 6. Audiobookshelf: the import inline (its empty Connect form, on now the scan + // has finished) and, below it, the not-matched offer once the issues load + // (community metadata is on and no book is matched). No Audiobookshelf is + // needed; Continue records the step done. + await page.getByLabel('Audiobookshelf address').waitFor({timeout: 15000}); + await page.getByRole('link', {name: 'Match automatically'}).waitFor({timeout: 15000}).catch(() => {}); + await sleep(600); + await tall('setup/abs.png'); await leave('Continue', 'Backups and updates'); // 7. Backups and updates. diff --git a/screenshots/manifest.mjs b/screenshots/manifest.mjs index d89a7727..ca553df7 100644 --- a/screenshots/manifest.mjs +++ b/screenshots/manifest.mjs @@ -189,7 +189,7 @@ export const SHOTS = [ {file: 'setup/naming.png', capture: 'setup', title: 'Setup - Book names', hint: 'step 3: four sample books named from tags and from folders side by side (the disagreeing rows marked), Folder names first preselected as Suggested (as tall as the page)'}, {file: 'setup/metadata.png', capture: 'setup', title: 'Setup - Community metadata', hint: 'step 4: community metadata on, the marketplace preselected from the browser (en-GB: United Kingdom), the service or a local copy, and the line saying Continue adds the library'}, {file: 'setup/reach.png', capture: 'setup', title: 'Setup - Addresses', hint: 'step 5, the library\'s scan done above it: the address the browser uses (a .local name) with Use as the home / public address, the two address fields, HTTPS: Off set by the config'}, - {file: 'setup/abs.png', capture: 'setup', title: 'Setup - Audiobookshelf', hint: 'step 6: what the Audiobookshelf import brings across, metadata.json read as it is, Open the import'}, + {file: 'setup/abs.png', capture: 'setup', title: 'Setup - Audiobookshelf', hint: 'step 6: what the Audiobookshelf import brings across, metadata.json read as it is, the import inline (the empty Connect to Audiobookshelf form, Connect on once the scan has finished), then the N books not matched offer with Match automatically'}, {file: 'setup/backups.png', capture: 'setup', title: 'Setup - Backups and updates', hint: 'step 7: the backup schedule (daily at 03:00), how many to keep (7), the backup folder (shown, set in config) and the update check'}, {file: 'setup/bridge.png', capture: 'setup', title: 'Setup - Add a Bridge', hint: 'step 8 with no Bridge paired: what a Bridge does, the compose snippet, Connect a bridge, Skip (as tall as the page)'}, {file: 'setup/phone.png', capture: 'setup', title: 'Setup - Connect a phone', hint: 'step 9, the last: the pairing QR code, New code, How signing in works with Invite someone, Open the web player, the ffmpeg line and the household note (as tall as the page)'}, diff --git a/static/img/screenshots/setup/abs.png b/static/img/screenshots/setup/abs.png index 0a929ace..539df15b 100644 Binary files a/static/img/screenshots/setup/abs.png and b/static/img/screenshots/setup/abs.png differ