mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
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:
@@ -275,38 +275,6 @@ function goToBook(bookId: number) {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
async function handleReimportDuplicate(fileId: number) {
|
||||
if (!result.value || reimportingIds.has(fileId)) return
|
||||
reimportingIds.add(fileId)
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
fileIds: [fileId],
|
||||
overrides: [{ fileId, skipDuplicateCheck: true }],
|
||||
}
|
||||
if (defaultLibraryId.value !== null) payload.defaultLibraryId = defaultLibraryId.value
|
||||
if (defaultFolderId.value !== null) payload.defaultFolderId = defaultFolderId.value
|
||||
const res = await api('/api/v1/book-dock/finalize', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!res.ok) return
|
||||
const newResult = await res.json()
|
||||
const fileResult = (newResult.results as typeof result.value.results)[0]
|
||||
if (!fileResult) return
|
||||
const entry = result.value.results.find((r) => r.fileId === fileId)
|
||||
if (entry) {
|
||||
Object.assign(entry, fileResult)
|
||||
if (fileResult.success) {
|
||||
result.value.succeeded++
|
||||
result.value.failed--
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reimportingIds.delete(fileId)
|
||||
}
|
||||
}
|
||||
|
||||
async function discardDuplicateSelection(payload: FinalizePayload): Promise<BookDockDiscardDuplicatesResult | null> {
|
||||
const res = await api('/api/v1/book-dock/finalize/discard-duplicates', {
|
||||
method: 'POST',
|
||||
@@ -578,15 +546,6 @@ async function handleDiscardResultDuplicates() {
|
||||
>
|
||||
{{ t('bookDock.finalizeDialog.viewExisting') }} <ExternalLink class="size-3" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!r.success && r.isDuplicate"
|
||||
class="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 shrink-0 transition-colors"
|
||||
:disabled="reimportingIds.has(r.fileId)"
|
||||
@click="handleReimportDuplicate(r.fileId)"
|
||||
>
|
||||
<Loader2 v-if="reimportingIds.has(r.fileId)" class="size-3 animate-spin" />
|
||||
{{ t('bookDock.finalizeDialog.importAnyway') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!r.success && !r.isDuplicate && r.message && !isFileExistsError(r.message)"
|
||||
class="text-xs text-red-500 flex items-center gap-1 shrink-0 hover:text-red-600 transition-colors"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import type { BookDockFinalizeResult } from '@bookorbit/types'
|
||||
import BookDockFinalizeDialog from '../BookDockFinalizeDialog.vue'
|
||||
|
||||
const apiMock = vi.fn<(...args: unknown[]) => Promise<Response>>()
|
||||
const finalizeResult = ref(null)
|
||||
const finalizeResult = ref<BookDockFinalizeResult | null>(null)
|
||||
const finalizeLoading = ref(false)
|
||||
const finalizeError = ref<string | null>(null)
|
||||
const finalizeMock = vi.fn<(...args: unknown[]) => Promise<void>>()
|
||||
@@ -97,7 +98,7 @@ describe('BookDockFinalizeDialog', () => {
|
||||
fileName: 'duplicate.epub',
|
||||
status: 'duplicate',
|
||||
existingBookId: 42,
|
||||
message: 'Duplicate: this book already exists in the library',
|
||||
message: 'A file with this name already exists at the target location',
|
||||
},
|
||||
{ fileId: 11, fileName: 'ready.epub', newName: 'ready.epub', status: 'ready' },
|
||||
],
|
||||
@@ -148,4 +149,32 @@ describe('BookDockFinalizeDialog', () => {
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('1 already in library')
|
||||
})
|
||||
|
||||
it('offers navigation and rename for an indexed destination collision without an import-anyway action', async () => {
|
||||
finalizeResult.value = {
|
||||
total: 1,
|
||||
succeeded: 0,
|
||||
failed: 1,
|
||||
results: [
|
||||
{
|
||||
fileId: 10,
|
||||
fileName: 'duplicate.epub',
|
||||
newName: 'Author/Title/duplicate.epub',
|
||||
success: false,
|
||||
isDuplicate: true,
|
||||
existingBookId: 42,
|
||||
message: 'A file with this name already exists at the target location',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const wrapper = mountDialog()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('View existing')
|
||||
expect(wrapper.text()).toContain('Already exists - save as')
|
||||
expect(wrapper.text()).toContain('Import')
|
||||
expect(wrapper.text()).not.toContain('Import anyway')
|
||||
expect(wrapper.find('input[type="text"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4016,7 +4016,6 @@
|
||||
"failedWithDuplicates": "{failed} fehlgeschlagen ({count} Duplikate) | {failed} fehlgeschlagen ({count} Duplikat) | {failed} fehlgeschlagen ({count} Duplikate)",
|
||||
"view": "Anzeigen",
|
||||
"viewExisting": "Vorhandenes anzeigen",
|
||||
"importAnyway": "Trotzdem importieren",
|
||||
"hide": "Ausblenden",
|
||||
"details": "Details",
|
||||
"alreadyExistsSaveAs": "Existiert bereits - speichern als:",
|
||||
|
||||
@@ -3974,7 +3974,6 @@
|
||||
"failedWithDuplicates": "{failed} failed ({count} duplicates) | {failed} failed ({count} duplicate) | {failed} failed ({count} duplicates)",
|
||||
"view": "View",
|
||||
"viewExisting": "View existing",
|
||||
"importAnyway": "Import anyway",
|
||||
"hide": "Hide",
|
||||
"details": "Details",
|
||||
"alreadyExistsSaveAs": "Already exists - save as:",
|
||||
|
||||
@@ -4016,7 +4016,6 @@
|
||||
"failedWithDuplicates": "{failed} mislukt ({count} duplicaten) | {failed} mislukt ({count} duplicaat) | {failed} mislukt ({count} duplicaten)",
|
||||
"view": "Bekijken",
|
||||
"viewExisting": "Bestaande bekijken",
|
||||
"importAnyway": "Toch importeren",
|
||||
"hide": "Verbergen",
|
||||
"details": "Details",
|
||||
"alreadyExistsSaveAs": "Bestaat al - opslaan als:",
|
||||
|
||||
@@ -4016,7 +4016,6 @@
|
||||
"failedWithDuplicates": "{failed} falhou ({count} duplicatas) | {failed} falhou ({count} duplicata) | {failed} falharam ({count} duplicatas)",
|
||||
"view": "Ver",
|
||||
"viewExisting": "Ver existente",
|
||||
"importAnyway": "Importar de qualquer forma",
|
||||
"hide": "Esconder",
|
||||
"details": "Detalhes",
|
||||
"alreadyExistsSaveAs": "Já existe - salvar como:",
|
||||
|
||||
@@ -4016,7 +4016,6 @@
|
||||
"failedWithDuplicates": "{failed} neuspešnih ({count} podvojenih) | {failed} neuspešna ({count} podvojena) | {failed} neuspešnih ({count} podvojenih)",
|
||||
"view": "Ogled",
|
||||
"viewExisting": "Ogled obstoječega",
|
||||
"importAnyway": "Vseeno uvozi",
|
||||
"hide": "Skrij",
|
||||
"details": "Podrobnosti",
|
||||
"alreadyExistsSaveAs": "Že obstaja - shrani kot:",
|
||||
|
||||
Reference in New Issue
Block a user