mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
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:
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user