feat(book-requests): add automated request fulfillment

Let users request books, route approvals, search configured sources, and import fulfilled releases through the Book Dock. Include scoped permissions, automation, download-client integrations, plugin support, requester visibility, and one consolidated schema migration.

Closes #248
Ref #1115
This commit is contained in:
neon
2026-08-31 16:33:40 +00:00
parent 0f5429131a
commit fc6e6f03d5
443 changed files with 87915 additions and 1730 deletions
-218
View File
@@ -1,218 +0,0 @@
# Migrating from Audiobookshelf
BookOrbit can perform a one-time import of reading state from Audiobookshelf. The migration matches
Audiobookshelf books to books that already exist in BookOrbit, maps Audiobookshelf users to existing
BookOrbit users, and imports the selected users' supported state.
This is an import, not a continuous synchronization. Keep both systems backed up until you have
reviewed the migration report and verified the imported state.
## Compatibility
Compatibility is declared only for versions covered by the disposable end-to-end suite.
| Mode | Verified Audiobookshelf version | Verification |
| -------- | ------------------------------- | ------------------------------------------------------------ |
| Live API | 2.36.0 | Full import, repeat-run idempotency, and database integrity |
| Backup | Backup produced by 2.36.0 | Live/backup normalization parity and dry-run planning parity |
Other Audiobookshelf versions may work, but they are not currently declared compatible. A missing
optional backup table produces a warning and omits that domain. A missing required table or column
causes validation to fail instead of importing incomplete or misidentified state.
Maintainers can exercise another image with:
```bash
ABS_E2E_IMAGE=ghcr.io/advplyr/audiobookshelf:<version> pnpm e2e:run -- migration-audiobookshelf
```
Do not broaden the compatibility declaration until both live API behavior and a backup produced by
that version pass the suite.
## Requirements
- The BookOrbit operator must have the `Manage App Settings` permission.
- Live API mode requires an Audiobookshelf access token for an `admin` or `root` user. Tokens for
regular and guest users are rejected.
- Target books must already exist in a scanned BookOrbit library. Unmatched source books are reported
and are not created automatically.
- Target BookOrbit user accounts must already exist. Audiobookshelf users are mapping candidates only;
accounts, passwords, roles, and permissions are not copied.
- Configure `MIGRATION_ENCRYPTION_KEY` with a persistent 64-character hexadecimal key before saving a
live API source. Without a valid key, saved source credentials are not encrypted at rest. Generate a
key with `openssl rand -hex 32` and retain it for as long as the saved source is needed.
## Supported data
The first compatibility declaration covers:
- book identity and metadata used by the migration, including title, subtitle, authors, narrators,
first series membership, ISBN, ASIN, publisher, year, language, description, genres, and tags;
- explicit user mapping to existing BookOrbit accounts;
- read, reading, and unread status from real Audiobookshelf progress records, including available start,
finish, and update dates;
- multi-file audiobook position, resolved to the correct target audio file and file-local second;
- EPUB percentage and EPUB CFI position;
- separate audio and EPUB positions for mixed-format books;
- audio bookmarks with their absolute book position, title, and creation time;
- valid listening sessions; and
- matching by ISBN, ASIN, mapped file path, then title and author.
The migration reports unmatched books, ambiguous matches, malformed source records, and state owned by
unmapped users. It does not assign that state to another user.
Metadata is applied as an overlay per field. A field Audiobookshelf actually carries replaces the value
on the matched BookOrbit book; a field Audiobookshelf leaves empty is skipped rather than written as
empty. An Audiobookshelf book with no publisher therefore leaves an existing BookOrbit publisher intact.
An explicit value is still an overlay: `abridged: false` and a zero duration are written.
Author sort names come from the `Last, First` form Audiobookshelf stores. A backup carries that per
author. The live API only exposes one joined sort string per book, so it is applied when the book has
exactly one author, and other books fall back to sorting on the display name.
## Deferred and unavailable data
The following data is not imported:
- Audiobookshelf podcasts and podcast sessions;
- user account creation, credentials, roles, or permissions;
- annotations, because Audiobookshelf does not expose an equivalent annotation domain;
- playlists and Audiobookshelf collections;
- covers from the live API or artwork from backups;
- PDF or comic reading locations that cannot be represented as EPUB CFIs;
- reader preferences, playback devices, server settings, or Audiobookshelf permissions; and
- continuous or two-way synchronization after the migration.
## Live API mode
Use live mode when BookOrbit can reach the running Audiobookshelf server.
1. Enter a clean `http` or `https` origin such as `https://audiobookshelf.example.com`. Do not include
credentials, a path, query parameters, or a fragment.
2. Enter an access token belonging to an Audiobookshelf `admin` or `root` user.
3. Enable private-network access only when the Audiobookshelf hostname resolves to a trusted local or
private address.
4. Test the connection, save the source, and validate it before creating the dry run.
BookOrbit resolves and pins the destination address for each request, rejects redirects, applies request
timeouts and response-size limits, and rejects private or local destinations unless private-network
access is explicitly enabled. Use HTTPS when traffic crosses an untrusted network. In Docker,
`127.0.0.1` refers to the BookOrbit container itself; use a hostname or address reachable from that
container.
The API token is redacted from migration API responses. It must never be placed in logs, screenshots,
reports, or issue comments.
## Backup mode
Use backup mode when BookOrbit should read an Audiobookshelf `.audiobookshelf` backup from local storage.
1. Create a fresh backup with Audiobookshelf's backup feature.
2. Create a dedicated host directory for migration inputs and copy the backup into it.
3. Mount that directory read-only into the BookOrbit container. For example, add this entry to the app
service's `volumes` list in `docker-compose.yml`:
```yaml
- /host/path/to/migration-imports:/imports:ro
```
4. Set `MIGRATION_IMPORT_ROOT=/imports` in BookOrbit's `.env` and restart BookOrbit.
5. In the migration source form, enter the absolute container path, such as
`/imports/audiobookshelf/backup.audiobookshelf` if that path exists under the mounted root.
`MIGRATION_IMPORT_ROOT` is a server-side authorization boundary. It must be an absolute path and cannot
be supplied through the migration request. BookOrbit resolves the configured root and backup path,
rejects paths and symlink targets outside the root, detects path swaps, requires a regular file, and
reads the archive through the already-authorized file handle. Use a dedicated read-only mount rather
than a broad filesystem root.
A compatible backup must be a ZIP archive containing exactly one `details` entry and one
`absdatabase.sqlite` entry. Archive entry counts and extracted sizes are bounded. BookOrbit copies only
the SQLite database to an owner-only temporary file and opens it read-only with SQLite defensive mode.
## Book and user mapping
Audiobookshelf stores paths as seen by the Audiobookshelf process. BookOrbit stores paths as seen by the
BookOrbit process. Add a prefix mapping when those paths differ. For example:
| Source prefix | Target prefix |
| ------------- | ------------------- |
| `/audiobooks` | `/books/audiobooks` |
Mappings replace only the matching leading prefix. More specific prefixes take priority. Save and
validate the mappings before starting the live run. Path mapping is one matching strategy; ISBN, ASIN,
and title/author matching can still match books whose paths differ.
Review every suggested user mapping. Users left unmapped remain visible in the mapping workflow and
migration report, but their state is not imported. Do not map multiple people to one BookOrbit user
merely to clear a warning.
## Recommended workflow
1. Back up BookOrbit and Audiobookshelf.
2. Scan the target BookOrbit libraries and confirm the target book files are present.
3. Create and test an Audiobookshelf source in live API or backup mode.
4. Save the source and validate its capabilities.
5. Map source users to existing BookOrbit users.
6. Add and validate path mappings where mount paths differ.
7. Run a dry run and review matches, ambiguities, unmatched books, unmapped users, warnings, and per-user
totals.
8. Resolve ambiguous matches and rerun the dry run until the plan is current.
9. Start the live migration and export the final report.
10. Verify several audiobook, EPUB, mixed-format, finished, and bookmarked examples before retiring the
Audiobookshelf instance.
Repeating the same completed plan is designed to be idempotent, but it is not a substitute for reviewing
the first report and keeping backups.
## Troubleshooting
### The live server is unreachable
- Confirm the URL contains only an `http` or `https` origin and does not redirect.
- Test DNS and network reachability from the BookOrbit server or container, not only from a browser.
- In Docker, do not use `127.0.0.1` for a service running in another container or on the host.
- Confirm a reverse proxy permits the Audiobookshelf API endpoints and does not redirect them to a login
page.
- A request times out after the bounded connector timeout. Investigate routing, DNS, firewall, or proxy
behavior before retrying.
### Audiobookshelf privileges are insufficient
The access token must authorize as an Audiobookshelf `admin` or `root` user. Create or select an account
with that type, obtain its token, and retest the source. BookOrbit intentionally rejects tokens that can
read only the current user's state because a complete migration must enumerate users and their state.
### A private or local address is rejected
Private-network access is off by default. Enable it only when the configured Audiobookshelf destination
is trusted and intentionally hosted on a private or local network. BookOrbit validates every resolved
address and pins the selected address for the request to limit DNS rebinding.
### A backup path is missing or rejected
- Confirm `MIGRATION_IMPORT_ROOT` is set in the BookOrbit process and is absolute.
- Use the path inside the BookOrbit container, not the host path or a browser workstation path.
- Confirm the backup exists under the canonical import root and is readable by the BookOrbit process.
- Prefer a direct regular-file path. A symlink target that resolves outside the root is rejected.
- If the container uses a read-only root filesystem, mount the import directory explicitly as shown
above; BookOrbit uses its writable temporary directory for bounded extraction.
### A backup is incompatible
Create a fresh backup with the verified Audiobookshelf version and retry. The backup must include valid
`details` and `absdatabase.sqlite` entries and the required users, library items, books, and media
progress schema. Missing optional author, series, listening-session, or library-folder tables are
reported as warnings. Missing required schema fails validation.
Do not manually modify the archive or SQLite schema to bypass validation. Preserve the original backup
and report the Audiobookshelf version and sanitized validation message when requesting compatibility
support.
### Books or users remain unresolved
- Confirm the target library scan completed before the dry run.
- Add a path-prefix mapping if the two applications see different mount paths.
- Check ISBN and ASIN metadata on both sides.
- Resolve ambiguous book matches explicitly in the dry-run workflow.
- Create the target BookOrbit user before mapping. The migration does not create accounts.
+169
View File
@@ -0,0 +1,169 @@
# Book Requests
Book requests let readers ask for a book BookOrbit does not have. An approver decides on the
request, a release is picked from the sources you configured, a download client fetches it, and the
result is filed into a library.
Everything in this guide is operator configuration. Nothing here is on by default, and a fresh
install has no sources, no download clients and no automation.
## What BookOrbit ships
**BookOrbit ships no sources.** There is no bundled tracker, no default indexer, and no list of
sites to pick from. One adapter type is compiled in, `torznab`, and it is a protocol rather than a
source: it needs an address you supply, usually a Prowlarr or Jackett instance you run. Everything
else is an indexer plugin you install yourself.
Which sources you point BookOrbit at, and what you search for and download through them, is your
decision and your responsibility. BookOrbit does not provide or endorse indexer sources. Its URL,
file, and path safety checks still apply to every configured source.
## Permissions
| Permission | What it allows |
| --------------------------- | ----------------------------------------------------------- |
| `book_request_access` | File a request, see your own, join somebody else's |
| `manage_book_requests` | Approve, reject, and drive fulfilment on every request |
| `book_request_auto_approve` | Your own requests skip the approval queue |
| `book_request_self_fulfill` | Pick the release and download it yourself, with no approver |
The last three each imply `book_request_access`; granting one on its own would produce an account
that can approve requests but has no page to see them on.
Configuring sources, download clients and automation is separate again, and needs
`manage_app_settings`. Installing an indexer plugin needs a superuser: a plugin runs inside the
server process with the server's reach.
## Before anything can be saved: the encryption key
Set `BOOK_REQUEST_ENCRYPTION_KEY` before configuring a download client or an indexer:
```bash
openssl rand -hex 32
```
It must be a 64-character hex string. Unlike `MIGRATION_ENCRYPTION_KEY` and the email key, this one
is not optional in practice: a tracker API key or seedbox password is **refused rather than stored
in the clear** when the key is missing, so the settings form will not save.
Rotating or removing the key leaves stored credentials unreadable. They are not silently dropped:
the affected source refuses to be used and says why, so you can re-enter the credential rather than
discover the loss during a download.
## Sources
Settings > System > Requests > Sources.
Two kinds of row, added two ways, identical once they exist:
- **Torznab**: an address plus an API key. This is how you attach a Prowlarr or Jackett instance,
and one such instance can expose many trackers behind one row per tracker.
- **Plugins**: a single `.mjs` file installed through the page, loaded at boot from
`<APP_DATA_PATH>/plugins/indexers/<name>/index.mjs`. Plugins live in their own repositories and
are not distributed with BookOrbit.
Per source you can set the medium it is searched for, its categories, whether it is handed an ISBN
when the request has one, and a colour that marks its releases in the release picker.
Private network addresses are refused unless you opt the row in with **Allow private address**. A
self-hosted Prowlarr on the LAN is the common reason to; a public tracker resolving to a private
address is not.
Two health facts are shown per row, and they answer different questions:
- **Connected / Not reachable** is the last time you pressed **Test**, which makes a capabilities
call.
- **Searches failing** is how the last real searches went. A source can be reachable and still
refuse every search, and the badge counts consecutive failures so one blip reads differently
from a source that has been broken for a week.
## Download clients
Settings > System > Requests > Download clients. qBittorrent, Transmission and Deluge are
supported.
Each client needs at least one **path mapping**, including when BookOrbit and the client run on the
same host. A mapping translates the download directory the client reports into a path BookOrbit can
open, and it is also the root the import is allowed to read out of. On a single host that mapping
is an identity, `/downloads -> /downloads`; it still has to be stated, because "the same path" and
"no path stated" are different things and only one of them is safe to import from.
Hardlinking from the download directory into the library is what keeps a torrent seeding after the
book is filed. The mapping form can test whether a hardlink actually works between the two paths,
which is worth doing before the first grab: a mapping across filesystems copies instead, silently
doubling the space every book takes.
The same mount is necessary but is not sufficient. On Linux, `fs.protected_hardlinks=1` normally
requires the BookOrbit process user to own the completed source file or be able to write it. The
download client and BookOrbit commonly run with the same UID, or with a shared group that grants
BookOrbit write access to completed files. The mapping test creates its own BookOrbit-owned source
file, so it proves that the paths and mount permit hardlinks but cannot prove that a completed file
owned by a different user will be linkable. If the real file cannot be linked, BookOrbit falls back
to copying it and the book uses space twice while the original keeps seeding.
BookOrbit never stops a seed on its own. Removing a torrent from its client is an explicit action
with its own confirmation, and removing one that is still working also fails the request.
## Automation
Settings > System > Requests > Automation. Everything here is off or unset by default.
- **Auto-grab** picks a release without an approver, but only above a minimum score. Below that the
request waits, and the approver still sees the full ranked list.
- **Auto-retry** falls back to the next-best release when an automatic grab fails, bounded by a
maximum number of attempts per request.
- **Auto-search** re-searches an approved request nothing was found for. Without it a request is
searched exactly once, and a book whose first release is posted next month waits forever. The
interval doubles for each week a request has been waiting, up to a cap, and stops entirely after
a configurable age.
- **Import checking** scores what landed against what was asked for. Below the threshold the file
waits in the Book Dock and the request reads "needs review", where a person either files it
anyway or discards it. Switching this off files whatever was downloaded straight into the target
library.
- **Import formats** decides what to keep when one release carries the same book several times
over: every format, or only the target library's preferred one. A multipart audiobook is never
affected, since its parts are one book rather than competing editions.
- **Default destinations** are per medium, and are the lowest rung of the ladder: a requester's own
pinned destination and an explicit choice at request or approval time both win over them. Without
one, a request that names no library can never be approved.
- **Release profiles** are an ordered tier list per medium describing the edition you want. Empty
is the ordinary state and disengages the tier axis entirely, leaving auto-grab decided by score.
## The lifecycle
```
pending -> approved -> searching -> grabbed -> downloading -> importing -> available
```
- **pending**: waiting on an approver. Auto-approval and self-serve both skip it.
- **needs_review**: the import scored below the threshold and is sitting in the Book Dock. Two ways
out: file it anyway, or discard it, which removes the dock entry and fails the request.
- **failed**: nothing could be downloaded, or a person discarded the import. Still grabbable, so a
different release can be tried.
- **rejected** / **cancelled**: settled by a person. A settled work can be requested again.
One live request per work. A second person asking for the same book is attached to the existing
request as a subscriber rather than opening a second one, and can leave it again from the request's
own menu.
## Troubleshooting
**"No sources are enabled"** on the requests page: either nothing is configured, or every row is
switched off. The notice says which.
**Requests sit at `approved` and nothing happens**: auto-grab is off, which is the default. Either
an approver opens the release picker, or auto-grab is switched on with a minimum score.
**A grab is refused with a path error**: the client has no path mapping, or the download directory
resolved outside the one it has. Add or widen the mapping under Settings > System > Requests.
**Credentials will not save**: `BOOK_REQUEST_ENCRYPTION_KEY` is unset or is not 64 hex characters.
**A source shows "Searches failing" but tests fine**: the address answers a capabilities call and
refuses searches. Usually a rate limit, an expired session, or categories that do not exist on that
tracker.
## Related
- [Development guide](DEVELOPMENT.md)
- [Testing guide](TESTING.md)
-290
View File
@@ -1,290 +0,0 @@
# Migrating from Calibre-Web Automated
BookOrbit can perform a one-time import from Calibre-Web Automated, abbreviated CWA. The migration
matches CWA books to books that already exist in BookOrbit, maps CWA users to existing BookOrbit users,
and imports supported reading state and static shelves.
This is an import, not continuous synchronization. Keep the original CWA installation and both systems'
backups until you have reviewed the migration report and verified the imported state.
## Compatibility
Compatibility is declared only for snapshots covered by the disposable end-to-end suite.
| Snapshot producer | Verified result |
| ----------------- | -------------------------------------------------------------------------------------- |
| CWA v4.0.6 | Full stopped-snapshot import, repeat-run idempotency, user isolation, and DB integrity |
CWA's `app.db` and Calibre's `metadata.db` do not contain a trustworthy CWA release number. BookOrbit
therefore reports the source version as unknown and emits this expected warning:
```text
Schema compatibility was verified against Calibre-Web Automated v4.0.6
```
That warning does not mean validation failed. Other CWA versions may work, but they are not currently
declared compatible. Missing optional tables produce precise warnings and disable only the affected
domain. Missing core book, file, or user columns cause validation to fail.
Maintainers can exercise another literal published image tag with:
```bash
CWA_E2E_IMAGE=crocodilestick/calibre-web-automated:<published-tag> \
pnpm e2e:run -- migration-calibre-web-automated
```
CWA's historical Docker tag casing is not consistent. Use the exact published tag rather than building
one from a version number. Do not broaden the compatibility declaration until the complete suite passes.
## Requirements
- The BookOrbit operator must have the `Manage App Settings` permission.
- Target books must already exist in a scanned BookOrbit library. Unmatched source books are reported
and are not created automatically.
- Target BookOrbit user accounts must already exist. CWA users are mapping candidates only; accounts,
passwords, roles, tokens, sessions, and permissions are never copied.
- BookOrbit must have a dedicated server-side import directory configured through
`MIGRATION_IMPORT_ROOT`.
- CWA must be fully stopped while `app.db` and its corresponding `metadata.db` are copied.
The two databases are independent SQLite files. Neither CWA nor BookOrbit can make a live copy of them
represent one shared instant. BookOrbit can detect active journal or WAL files beside the submitted
snapshots, but it cannot prove that snapshots copied elsewhere were taken consistently. Stopping CWA
before the copy is therefore required even when validation succeeds.
## Supported data
The first compatibility declaration covers:
- non-Guest CWA users as explicit mapping candidates, including administrators and regular users;
- Calibre books and individual formats;
- title, ordered authors and sort names, ISBN, ASIN and supported provider identifiers, publisher,
publication year, language, series, rating, description, and tags;
- unread, reading, and read status with available start, finish, and update dates;
- web-reader EPUB CFI resume position;
- web-reader audio position converted from milliseconds to seconds;
- web-reader comic position converted from CWA's zero-based image index to BookOrbit's one-based page;
- Kobo EPUB or KEPUB progress, preferring KEPUB when both formats exist;
- KOReader percentage and compatible EPUB locator when CWA's checksum index maps the document uniquely;
- static shelves and their relative book order; and
- matching by ISBN, ASIN, mapped file path, then title and author.
Calibre tags are imported as BookOrbit tags, not genres. Metadata is applied as an overlay: a field CWA
actually carries can replace that field on the matched BookOrbit book, while unavailable source fields
are left unchanged.
CWA's table named `bookmark` stores one current web-reader resume position. It does not represent a
collection of named bookmarks, so those rows become reading progress rather than BookOrbit bookmarks.
## Deferred and unavailable data
The following data is not imported:
- the default Guest account or state owned only by Guest;
- account credentials, OAuth data, API tokens, mail secrets, sessions, device secrets, or Hardcover
tokens;
- user creation, roles, permissions, or authentication settings;
- narrators and genres, because CWA does not distinguish them reliably from Calibre tags;
- reading-session history;
- named bookmarks and annotations;
- covers;
- Magic Shelves, dynamic shelf rules, and public-sharing behavior;
- arbitrary Calibre custom columns;
- CWA operational data from `cwa.db`; and
- continuous or two-way synchronization after the migration.
A public CWA shelf is imported as a private collection owned by the mapped BookOrbit user. It is never
granted to another user automatically.
## Create a stopped-instance snapshot
The common CWA container paths are:
| Data | Typical CWA container path |
| -------------------- | ------------------------------ |
| CWA application data | `/config/app.db` |
| Calibre catalog | `/calibre-library/metadata.db` |
Use the host paths bound to those container locations. If CWA selects a library below another folder,
copy the `metadata.db` belonging to the library currently configured in CWA, not another Calibre
catalog found on the host.
1. Back up the CWA config and Calibre library using your normal backup process.
2. Stop CWA completely. With the standard Compose service name:
```bash
docker compose stop calibre-web-automated
```
3. Confirm the container is stopped. Replace the name if your deployment uses another container name:
```bash
docker inspect -f '{{.State.Running}}' calibre-web-automated
```
The command must print `false`.
4. Inspect both source directories for non-empty SQLite sidecars. Check for `app.db-wal`,
`app.db-journal`, `metadata.db-wal`, and `metadata.db-journal`. Do not copy only a main database while
one of these contains data. Do not delete a sidecar to make validation pass. Restart CWA, allow its
recovery and startup work to finish, stop it cleanly, and check again.
5. While CWA remains stopped, copy exactly the two main database files into a dedicated BookOrbit import
directory. For example:
```bash
install -d -m 700 /host/path/to/bookorbit-migration-imports/cwa
install -m 600 /host/path/to/cwa-config/app.db \
/host/path/to/bookorbit-migration-imports/cwa/app.db
install -m 600 /host/path/to/calibre-library/metadata.db \
/host/path/to/bookorbit-migration-imports/cwa/metadata.db
```
6. Confirm the copied files are different regular files and that no copied `-wal` or `-journal`
sidecars exist beside them.
7. CWA may be restarted after both copies are complete. BookOrbit reads only the copies.
If CWA was never allowed to finish startup migrations, optional KOReader tables may not exist. When
KOReader progress matters, let the same CWA version start fully and finish its checksum work before
stopping and recopying the databases.
## Configure the BookOrbit import root
Mount only the dedicated import directory read-only into the BookOrbit app container. For example, add
this entry to the app service's `volumes` list in `docker-compose.yml`:
```yaml
- /host/path/to/bookorbit-migration-imports:/imports:ro
```
Set the corresponding absolute container path in BookOrbit's `.env`:
```dotenv
MIGRATION_IMPORT_ROOT=/imports
```
Restart BookOrbit so it receives the environment value and mount. In the migration source form, select
Calibre-Web Automated and enter:
```text
Application database: /imports/cwa/app.db
Calibre metadata database: /imports/cwa/metadata.db
```
`MIGRATION_IMPORT_ROOT` is a server-side authorization boundary. It cannot be supplied or overridden by
the migration request. BookOrbit requires absolute paths, resolves the configured root and both files,
rejects symlinks or path traversal, requires two different regular files, copies them into owner-only
temporary storage, opens them read-only with SQLite defensive mode, runs integrity checks, and cleans up
the private copies after each operation.
Use paths as seen by the BookOrbit process or container. A host path that is not mounted at the same
location is not a valid migration path. Do not use `/`, a home directory, the CWA config directory, or a
broad storage mount as the import root.
## Book path mapping
The snapshot file location is not the source library root used for book matching. Book paths are based
on the logical Calibre library root stored in CWA's `app.db`, normally `/calibre-library`. A split-library
configuration may provide a different logical root. BookOrbit reports the detected prefix after source
validation.
Map that CWA-visible prefix to the path where the same files exist inside BookOrbit. For example:
| Source prefix | Target prefix |
| ------------------ | ------------- |
| `/calibre-library` | `/books` |
Mappings replace only the matching leading path prefix. The target prefix is not the host path unless
BookOrbit sees that exact path. Validate the mapping before creating the final dry run.
CWA does not provide BookOrbit file hashes, so mapped paths are especially important for books with
multiple files of the same media kind, such as EPUB and KEPUB together. ISBN, ASIN, and title/author
matching can still match books whose paths differ, but exact mapped paths give file-level progress the
strongest target-file signal.
## User mapping and recommended workflow
Review every suggested user mapping. Users left unmapped remain visible in source counts and reports,
but their status, progress, and shelves are not imported. Never map multiple people to one BookOrbit
account merely to clear a warning.
1. Back up BookOrbit and CWA.
2. Scan the target BookOrbit libraries and confirm the target files are present.
3. Stop CWA and create both snapshots as described above.
4. Configure the read-only import mount and `MIGRATION_IMPORT_ROOT`, then restart BookOrbit.
5. Create a Calibre-Web Automated source with the two snapshot paths.
6. Test the source, save it, and validate its capabilities and warnings.
7. Map selected CWA users to existing BookOrbit users.
8. Add and validate the logical-library path mapping.
9. Run a dry run and review match strategies, ambiguities, unmatched books, unmapped users, warnings,
unavailable domains, and per-user totals.
10. Resolve ambiguous matches and create a current dry run.
11. Start the live migration and export the final report.
12. Verify representative EPUB, Kobo or KEPUB, KOReader, audio, comic, finished-status, and ordered-shelf
examples before retiring the CWA installation or snapshots.
Repeating the same completed plan is designed to be idempotent, but it is not a substitute for reviewing
the first report and retaining backups.
## Troubleshooting
### A snapshot is reported as active or hot
BookOrbit rejects a submitted database when a non-empty `-wal` or `-journal` file exists beside it. Stop
CWA, discard the attempted copies, and create a fresh pair from the stopped installation. Do not remove
sidecars from a running installation or copy only the main database.
If the original stopped directories still contain non-empty sidecars, restart the same CWA installation,
allow recovery to complete, stop it cleanly, and inspect again. Preserve the original files and request
support if they remain; do not repair production databases in place merely to satisfy migration.
### The two database paths appear reversed
The application field must point to CWA's `app.db`, which contains the `user` table. The metadata field
must point to Calibre's `metadata.db`, which contains `books` and `data`. Correct the two fields and
retest. Renaming the wrong files does not make them compatible.
### A path is missing, outside the import root, or unreadable
- Confirm `MIGRATION_IMPORT_ROOT` is set in the BookOrbit process and is absolute.
- Use paths inside the BookOrbit container, not host-only or workstation paths.
- Confirm the read-only mount contains both files and the BookOrbit container user can read them.
- Keep both snapshots below the canonical import root and avoid symlinks.
- Confirm the paths identify two different regular files.
### The snapshot schema is incompatible
Confirm the files came from the same stopped CWA installation and that CWA had completed startup. Missing
required user, book, or file tables or columns fail validation. Do not edit SQLite tables or bypass
validation. Preserve the original snapshot pair and report the exact CWA image tag plus the sanitized
validation message when requesting compatibility support.
### Optional progress or shelves are unavailable
Missing `book_read_link`, web bookmark, Kobo, KOReader, checksum, or shelf tables disable only the related
domain and produce a warning. This can be legitimate when the feature was never used. If the source
installation should contain that data, start the same CWA version, allow its migrations and checksum work
to finish, stop it, and create a new snapshot pair.
### No CWA-specific schema signature is found
BookOrbit warns when the snapshot has compatible core Calibre-Web tables but none of the known CWA
signatures. The files may come from stock Calibre-Web or an unverified CWA schema. Review the warning and
source provenance carefully. A core-schema match is not proof of CWA compatibility.
### The logical library path is unsafe or missing
BookOrbit rejects traversal, backslashes, NULs, and unsafe absolute or relative path components. The
logical root comes from CWA configuration, not from the snapshot's host directory. Correct the library
configuration in CWA, let it start successfully, stop it, and create fresh snapshots rather than editing
the copies.
### Books or users remain unresolved
- Confirm the target BookOrbit library scan completed before the dry run.
- Map the CWA logical library prefix to the path visible inside BookOrbit.
- Check ISBN and ASIN metadata on both sides.
- Check title and author spelling when relying on the final fallback strategy.
- Resolve ambiguous book matches explicitly in the dry-run workflow.
- Create the target BookOrbit account before mapping. The migration does not create users.
Unmatched books remain in the report and do not prevent valid matched books from importing.