feat(scanner): align bookdock duplicate detection with upload behavior

Replace metadata-based duplicate checks (ISBN, title/author) during BookDock finalization with destination path lookup. This allows books with shared or incorrect metadata to be imported whenever their resolved library target paths differ, matching the standard upload workflow.

Closes #736
This commit is contained in:
neon
2026-07-22 20:06:49 +00:00
parent fb93f6ec20
commit 58c5b422d3
14 changed files with 349 additions and 622 deletions
@@ -404,8 +404,8 @@ describe('Book Dock ingest + finalize (e2e)', () => {
const existingBookId = seedBody.results[0]!.bookId!;
const duplicateRow = await createBookDockRow(context, {
fileName: 'duplicate-input.fb2',
selectedMetadata: { title: 'Duplicate Candidate Title', authors: ['Another Author'], isbn13: '9780306406157' },
fileName: 'seed-duplicate.fb2',
selectedMetadata: { title: 'Duplicate Seed Title', authors: ['Duplicate Author'], isbn13: '9780306406157' },
targetLibraryId: destination.libraryId,
targetFolderId: destination.libraryFolderId,
});
@@ -491,8 +491,8 @@ describe('Book Dock ingest + finalize (e2e)', () => {
const existingBookId = seedBody.results[0]!.bookId!;
const duplicateRow = await createBookDockRow(context, {
fileName: 'preview-duplicate-input.fb2',
selectedMetadata: { title: 'Preview Duplicate Candidate', authors: ['Different Author'], isbn13: '9780306406157' },
fileName: 'preview-seed-duplicate.fb2',
selectedMetadata: { title: 'Preview Duplicate Seed', authors: ['Preview Author'], isbn13: '9780306406157' },
targetLibraryId: destination.libraryId,
targetFolderId: destination.libraryFolderId,
});
@@ -572,12 +572,12 @@ describe('Book Dock ingest + finalize (e2e)', () => {
expect(await fileExists(readyRow.absolutePath)).toBe(true);
});
it('finalize allows same title with different author when isbn is missing', async () => {
it('finalize allows the same ISBN when resolved destinations differ', async () => {
const destination = await createLibraryWithFolder(context);
const seedRow = await createBookDockRow(context, {
fileName: 'seed-same-title.fb2',
selectedMetadata: { title: 'Shared Title', authors: ['Author One'] },
fileName: 'same-isbn-first.fb2',
selectedMetadata: { title: 'Same ISBN First', authors: ['Author One'], isbn13: '9780306406157' },
targetLibraryId: destination.libraryId,
targetFolderId: destination.libraryFolderId,
});
@@ -593,8 +593,8 @@ describe('Book Dock ingest + finalize (e2e)', () => {
const existingBookId = seedBody.results[0]!.bookId!;
const candidateRow = await createBookDockRow(context, {
fileName: 'candidate-same-title.fb2',
selectedMetadata: { title: 'Shared Title', authors: ['Author Two'] },
fileName: 'same-isbn-second.fb2',
selectedMetadata: { title: 'Same ISBN Second', authors: ['Author Two'], isbn13: '9780306406157' },
targetLibraryId: destination.libraryId,
targetFolderId: destination.libraryFolderId,
});
@@ -881,6 +881,21 @@ describe('Book Dock ingest + finalize (e2e)', () => {
message: expect.arrayContaining(['defaultLibraryId and defaultFolderId must either both be provided or both be omitted']),
});
const bypassValidationResponse = await context.app.inject({
method: 'POST',
url: '/api/v1/book-dock/finalize',
headers: authHeader(context.adminToken),
payload: {
fileIds: [123],
overrides: [{ fileId: 123, skipDuplicateCheck: true }],
},
});
expect(bypassValidationResponse.statusCode).toBe(400);
expect(bypassValidationResponse.json()).toMatchObject({
message: expect.arrayContaining(['overrides.0.property skipDuplicateCheck should not exist']),
});
const retriableErrorRow = await createBookDockRow(context, {
fileName: 'retry-fetch.fb2',
status: 'error',