From 58756565fc6f4d25512da0f16e54e722afb2e7a7 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:07:50 +0000 Subject: [PATCH] fix(kobo): preserve koreader matching for converted downloads Register the identity of delivered bytes so Kobo conversions and audio removal continue matching their source books in KOReader. Keep download registration recoverable without allowing database lock waits to stall it. Preserve manual corrections across cached device identities, make review semantics explicit, and keep cache invalidation bounded for large libraries. Add unit, database, lifecycle and scale regressions with one generated migration. Closes #1697 --- .../features/settings/KoreaderSettings.vue | 27 +- .../__tests__/KoreaderSettings.spec.ts | 14 +- client/src/locales/en.json | 18 +- docs/KOREADER_HASH_MATCHING.md | 21 + scripts/e2e/suite-registry.mjs | 19 + .../src/common/utils/file-hash.utils.test.ts | 59 + server/src/common/utils/file-hash.utils.ts | 38 + server/src/db/book-file-hash-history.test.ts | 51 + server/src/db/book-file-hash-history.ts | 65 + ...03_fix_kobo_koreader_download_matching.sql | 5 + .../src/db/migrations/meta/0103_snapshot.json | 22717 ++++++++++++++++ server/src/db/migrations/meta/_journal.json | 7 + server/src/db/schema/books.ts | 1 + server/src/db/schema/koreader.ts | 3 +- server/src/db/schema/libraries.ts | 1 + .../architecture-boundaries.test.ts | 1 - .../modules/book-move/book-move.repository.ts | 3 +- .../src/modules/book/book.repository.test.ts | 14 +- server/src/modules/book/book.repository.ts | 8 +- .../file-write/file-rename.repository.test.ts | 6 +- .../file-write/file-rename.repository.ts | 3 +- .../file-write/file-write.repository.ts | 3 +- .../kobo/kobo-download.repository.test.ts | 95 + .../modules/kobo/kobo-download.repository.ts | 56 + server/src/modules/kobo/kobo.module.test.ts | 8 + server/src/modules/kobo/kobo.module.ts | 8 + .../services/kepub-conversion.service.test.ts | 176 +- .../kobo/services/kepub-conversion.service.ts | 41 +- .../kobo-download-file-handle.test.ts | 75 + ...obo-download-hash-backfill.service.test.ts | 187 + .../kobo-download-hash-backfill.service.ts | 110 + ...download-hash-registration.service.test.ts | 237 + ...kobo-download-hash-registration.service.ts | 152 + .../services/kobo-download.service.test.ts | 631 +- .../kobo/services/kobo-download.service.ts | 64 +- .../koreader-hash-link.service.test.ts | 67 +- .../koreader/koreader-hash-link.service.ts | 61 +- .../koreader-plugin.repository.test.ts | 51 +- .../koreader/koreader-plugin.repository.ts | 23 +- .../koreader/koreader-plugin.service.test.ts | 126 +- .../koreader/koreader-plugin.service.ts | 23 +- .../koreader/koreader.repository.test.ts | 228 +- .../modules/koreader/koreader.repository.ts | 160 +- .../src/modules/library/library.repository.ts | 5 +- server/src/modules/scanner/lib/hash.ts | 34 +- .../scanner/scanner.repository.test.ts | 3 +- .../src/modules/scanner/scanner.repository.ts | 5 +- .../upload/upload-processor.service.test.ts | 11 +- .../upload/upload-processor.service.ts | 7 +- .../test/kobo-koreader-download.e2e-spec.ts | 539 + .../test/koreader-bulk-manifest.e2e-spec.ts | 29 + 51 files changed, 25561 insertions(+), 735 deletions(-) create mode 100644 docs/KOREADER_HASH_MATCHING.md create mode 100644 server/src/common/utils/file-hash.utils.test.ts create mode 100644 server/src/common/utils/file-hash.utils.ts create mode 100644 server/src/db/book-file-hash-history.test.ts create mode 100644 server/src/db/book-file-hash-history.ts create mode 100644 server/src/db/migrations/0103_fix_kobo_koreader_download_matching.sql create mode 100644 server/src/db/migrations/meta/0103_snapshot.json create mode 100644 server/src/modules/kobo/kobo-download.repository.test.ts create mode 100644 server/src/modules/kobo/kobo-download.repository.ts create mode 100644 server/src/modules/kobo/services/kobo-download-file-handle.test.ts create mode 100644 server/src/modules/kobo/services/kobo-download-hash-backfill.service.test.ts create mode 100644 server/src/modules/kobo/services/kobo-download-hash-backfill.service.ts create mode 100644 server/src/modules/kobo/services/kobo-download-hash-registration.service.test.ts create mode 100644 server/src/modules/kobo/services/kobo-download-hash-registration.service.ts create mode 100644 server/test/kobo-koreader-download.e2e-spec.ts diff --git a/client/src/features/settings/KoreaderSettings.vue b/client/src/features/settings/KoreaderSettings.vue index 42093cf01..d928d2162 100644 --- a/client/src/features/settings/KoreaderSettings.vue +++ b/client/src/features/settings/KoreaderSettings.vue @@ -454,9 +454,9 @@ async function handleRefreshUnmatched() { try { await fetchUnmatchedBooks() unmatchedPage.value = 1 - toast.success(t('settings.reader.koreader.hashLinks.toast.unmatchedRefreshed')) + toast.success(t('settings.reader.koreader.hashLinks.toast.reviewRefreshed')) } catch { - toast.error(t('settings.reader.koreader.hashLinks.toast.unmatchedRefreshFailed')) + toast.error(t('settings.reader.koreader.hashLinks.toast.reviewRefreshFailed')) } } @@ -499,10 +499,10 @@ async function handleDismissUnmatchedBook() { dismissingHash.value = dismissConfirmBook.value.hash try { await dismissUnmatchedBook(dismissConfirmBook.value.hash) - toast.success(t('settings.reader.koreader.hashLinks.toast.unmatchedDismissed')) + toast.success(t('settings.reader.koreader.hashLinks.toast.reviewDismissed')) handleCloseDismiss() } catch (e) { - toast.error(e instanceof Error ? e.message : t('settings.reader.koreader.hashLinks.toast.dismissFailed')) + toast.error(e instanceof Error ? e.message : t('settings.reader.koreader.hashLinks.toast.reviewDismissFailed')) } finally { dismissingHash.value = null } @@ -521,14 +521,14 @@ async function handleDismissAllUnmatchedBooks() { try { const result = await dismissAllUnmatchedBooks() toast.success( - t('settings.reader.koreader.hashLinks.toast.allDismissed', { + t('settings.reader.koreader.hashLinks.toast.reviewAllDismissed', { count: result.count, }), ) dismissAllConfirmOpen.value = false unmatchedPage.value = 1 } catch (e) { - toast.error(e instanceof Error ? e.message : t('settings.reader.koreader.hashLinks.toast.dismissAllFailed')) + toast.error(e instanceof Error ? e.message : t('settings.reader.koreader.hashLinks.toast.reviewDismissAllFailed')) } finally { dismissingAll.value = false } @@ -1152,7 +1152,7 @@ async function handleDownloadPlugin() {

- {{ t('settings.reader.koreader.hashLinks.unmatchedTitle') }} + {{ t('settings.reader.koreader.hashLinks.reviewTitle') }}

+

{{ t('settings.reader.koreader.hashLinks.reviewHint') }}

- {{ t('settings.reader.koreader.hashLinks.loadingUnmatched') }} + {{ t('settings.reader.koreader.hashLinks.loadingReview') }}
- {{ t('settings.reader.koreader.hashLinks.noUnmatched') }} + {{ t('settings.reader.koreader.hashLinks.noReview') }}