feat(server): add Audiobookshelf and Calibre-Web Automated migration sources

Extend the migration module beyond Booklore and Grimmory so an existing Audiobookshelf or
Calibre-Web Automated library can be moved into BookOrbit without rebuilding user state by
hand. Audiobookshelf supports both a live API connection and an offline backup file, while
Calibre-Web Automated reads a stopped snapshot of the app and metadata databases.

Match source books by ASIN in addition to ISBN, and batch the ISBN, ASIN, file hash, file
path, and title/author lookups so planning a large library issues a bounded number of
queries rather than one per book. Route the batched author comparison through the shared
accent-insensitive predicate, which now accepts a SQL fragment so a per-row pattern column
is matched the same way a bound literal is.

Extract the shared source connection form into MigrationSourceFields so the modal and the
settings page stay in sync as source types are added, and allow a mapping to skip a source
user instead of forcing every account to be imported.

Closes #986
This commit is contained in:
neon
2026-08-16 05:15:34 +00:00
parent 5527852b4b
commit ec4072e4be
87 changed files with 13988 additions and 698 deletions
+6 -1
View File
@@ -49,7 +49,8 @@ function printSuiteList() {
console.log("- all [composite] - Run all suites sequentially");
for (const suite of suites) {
const dbMode = suite.prepareDedicatedDatabase ? "dedicated-db" : "default-db";
console.log(`- ${suite.id} [${dbMode}] - ${suite.description}`);
const requirements = [dbMode, ...(suite.requiresDocker ? ["docker"] : [])].join(", ");
console.log(`- ${suite.id} [${requirements}] - ${suite.description}`);
}
}
@@ -109,6 +110,10 @@ async function main() {
mkdirSync(resultsDir, { recursive: true });
const e2eDatabaseUrl = process.env.E2E_DATABASE_URL ?? DEFAULT_E2E_DATABASE_URL;
if (suites.some((suite) => suite.requiresDocker)) {
console.log("Verifying Docker availability...");
await runCommand("docker", ["version", "--format", "{{.Server.Version}}"]);
}
if (!isCiEnvironment() && suites.some((suite) => suite.prepareDedicatedDatabase)) {
console.log("Starting local PostgreSQL (dev compose)...");
await runCommand("pnpm", ["run", "db:up"]);
+38
View File
@@ -209,6 +209,44 @@ export const E2E_SUITES = Object.freeze({
...SHARED_DB_AND_HELPER_PATHS,
],
},
"migration-audiobookshelf": {
id: "migration-audiobookshelf",
name: "Migration Audiobookshelf",
timeout: 90,
lane: "full",
description: "Audiobookshelf live API and real-backup migration end-to-end suite",
vitestTarget: "test/migration-audiobookshelf.e2e-spec.ts",
junitOutput: `${TEST_RESULTS_DIR}/migration-audiobookshelf-e2e-junit.xml`,
prepareDedicatedDatabase: true,
useDedicatedDatabase: true,
requiresDocker: true,
changedPaths: [
"server/src/modules/migration/**",
"server/test/migration-audiobookshelf.e2e-spec.ts",
"server/test/e2e/migration-audiobookshelf/**",
".github/workflows/e2e-runner.yml",
...SHARED_DB_AND_HELPER_PATHS,
],
},
"migration-calibre-web-automated": {
id: "migration-calibre-web-automated",
name: "Migration Calibre-Web Automated",
timeout: 120,
lane: "full",
description: "Calibre-Web Automated stopped-snapshot migration end-to-end suite",
vitestTarget: "test/migration-calibre-web-automated.e2e-spec.ts",
junitOutput: `${TEST_RESULTS_DIR}/migration-calibre-web-automated-e2e-junit.xml`,
prepareDedicatedDatabase: true,
useDedicatedDatabase: true,
requiresDocker: true,
changedPaths: [
"server/src/modules/migration/**",
"server/test/migration-calibre-web-automated.e2e-spec.ts",
"server/test/e2e/migration-calibre-web-automated/**",
".github/workflows/e2e-runner.yml",
...SHARED_DB_AND_HELPER_PATHS,
],
},
"authorization-matrix": {
id: "authorization-matrix",
name: "Authorization Matrix",