feat(kobo/koreader): two way page level sync between kobo/koreader and bookorbit#250
Conversation
Implement two-way page-level reading progress synchronization between Kobo devices, KOReader, and BookOrbit. This ensures readers can switch between physical devices and the web app without losing their exact page-level reading position. Closes #249
Restore the main branch thumbnail-click setting after the Kobo branch accidentally dropped it during the rebase. Keep the Kobo architecture allowlist aligned with the new identity service.
📝 WalkthroughWalkthroughThis PR extends BookOrbit's reader ecosystem with two-way progress synchronization, KOReader XPointer support, Kobo identity mapping, KEPUB conversion caching, enriched progress metadata, and server-side sync/orchestration across controllers, services, repository, schema, and client UI/components. ChangesTwo-Way Progress Sync & Koreader Progress
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches📝 Generate docstrings
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
server/src/modules/kobo/services/kepub-conversion.service.ts (1)
28-28: 💤 Low valueConsider adding a fallback or explicit validation for the config value.
The non-null assertion assumes
storage.appDataPathis always present. If this config key is ever missing, the service will fail at runtime when constructing the cache path.♻️ Suggested improvement
- this.kepubCachePath = join(this.config.get<string>('storage.appDataPath')!, '.kepub-cache'); + const appDataPath = this.config.getOrThrow<string>('storage.appDataPath'); + this.kepubCachePath = join(appDataPath, '.kepub-cache');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/kobo/services/kepub-conversion.service.ts` at line 28, The assignment to this.kepubCachePath uses a non-null assertion on this.config.get('storage.appDataPath') which can throw at runtime; update the constructor (or the KepubConversionService initialization) to validate and provide a safe fallback: retrieve the value from config into a local variable (e.g. const appDataPath = this.config.get<string>('storage.appDataPath')), if it's missing either throw a clear Error mentioning 'storage.appDataPath' or set a sensible default (e.g. process.cwd() or configured storage dir) before calling join to build this.kepubCachePath, ensuring the code uses the validated appDataPath rather than the non-null assertion.server/src/modules/reader/epub/epub.service.test.ts (1)
361-382: ⚡ Quick winAdd regression tests for fallback branches introduced by KEPUB routing.
Please add coverage for: (1)
koboSettingsService.getSettingsrejection falling back to original EPUB, and (2)kepubConversionService.getKepubPathrejection fallback. These are key resilience paths in the new flow.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/reader/epub/epub.service.test.ts` around lines 361 - 382, Add two regression tests around service.getBookInfo to exercise the failure fallbacks: (1) simulate koboSettingsService.getSettings rejecting and assert that getBookInfo falls back to the original EPUB path (verify bookReadService.findFileById returned path is used and that mockStat and mockOpenFile were called with '/books/alt.epub' and that kepubConversionService.getKepubPath was not relied upon), and (2) simulate kepubConversionService.getKepubPath rejecting (while getSettings resolves with convertToKepub true) and assert getBookInfo falls back to the original EPUB path (verify kepubConversionService.getKepubPath was called with the expected args, then mockStat/mockOpenFile were called with '/books/alt.epub'). Ensure both tests mock bookReadService.findFileById like the existing test and use the same user and makeEpubArchive setup to mirror the success case.server/src/modules/kobo/services/kobo-book-identity.service.ts (1)
25-28: ⚡ Quick winReplace non-null assertion with explicit error.
The
!assertion on line 27 assumesbookIdwill always be present in the returned map. While this should be true in normal operation, if the identity is missing (due to a race condition or database issue), the runtime error would be unclear. Throw an explicit error instead.🛡️ Proposed fix
async ensureForBook(userId: number, bookId: number, needsLegacyNumericRemoval: boolean): Promise<KoboBookIdentity> { const identities = await this.ensureForBooks(userId, [bookId], needsLegacyNumericRemoval); - return identities.get(bookId)!; + const identity = identities.get(bookId); + if (!identity) { + throw new Error(`Failed to ensure identity for book ${bookId}`); + } + return identity; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/kobo/services/kobo-book-identity.service.ts` around lines 25 - 28, The ensureForBook method currently uses a non-null assertion on identities.get(bookId) which can hide missing-identity cases; update ensureForBook to check whether the map returned by ensureForBooks contains bookId (e.g., using identities.has(bookId) or checking the value !== undefined) and if missing throw a clear, explicit error (include identifiers like bookId and userId in the message) instead of using the `!` operator so callers get a descriptive failure; keep the method signature async ensureForBook(userId: number, bookId: number, needsLegacyNumericRemoval: boolean): Promise<KoboBookIdentity>.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@client/src/features/reader/epub/composables/useFoliate.ts`:
- Around line 187-190: The current logic sets didNavigate = true even when
view.goToFraction is absent (because the optional call is a no-op), preventing
the subsequent fallback goTo(0); change the guard so you only call
view.goToFraction and set didNavigate when the method actually exists and was
invoked: check that view.goToFraction is truthy before calling it (and only then
set didNavigate = true) while keeping the existing fallbackFraction checks;
update the block that references didNavigate, fallbackFraction and
view.goToFraction accordingly so the final goTo(0) can still run when
goToFraction is not available.
In `@server/src/modules/book/book.repository.ts`:
- Around line 1140-1153: The current code replaces the entire currentBookmark
object, which drops any existing device-authored keys (e.g., ChapterProgress);
instead, merge updates into the existing bookmark object rather than rebuilding
it from scratch: locate where currentBookmark is created and change the logic to
start from the existing bookmark (or a shallow clone of it) and then
set/overwrite only LastModified, ProgressPercent, Location and
ContentSourceProgressPercent (using the normalizedKobo... variables) so other
keys are preserved (e.g., via Object.assign or object spread into
currentBookmark before assigning Location and ContentSourceProgressPercent).
- Around line 1279-1281: The current logic treats a null
koboContentSourceProgressPercent as "no change", which prevents clearing a
stored source-level percent; update the condition in the comparison block (where
koboContentSourceProgressPercent,
extractKoboContentSourceProgressPercent(bookmark), and PROGRESS_EPSILON are
used) so that when koboContentSourceProgressPercent === null it only returns
true if the existingSourcePercent is also null (otherwise return false to allow
clearing); keep the existing epsilon-based comparison for the non-null incoming
percent path.
In `@server/src/modules/kobo/kobo-auth.controller.ts`:
- Around line 19-25: authDevice and authRefresh currently assume `@Body`() is
non-null and will throw if the request JSON is null; fix both by normalizing the
incoming body to an empty object before reading fields (e.g., at the top of
authDevice and authRefresh assign a local payload = body ?? {} and use
payload.UserKey / payload.RefreshToken instead of body.*), ensuring you still
call optionalString(payload.UserKey) and optionalString(payload.RefreshToken) so
null bodies don't cause a 500; update types if needed to accept KoboAuthBody |
null.
In `@server/src/modules/kobo/services/kepub-conversion.service.ts`:
- Around line 44-47: The execFileAsync call currently runs without a timeout and
can hang; update the execFileAsync(binaryPath, args) invocation to pass an
options object with a sensible timeout (e.g., { timeout: 30_000 }) so the child
process is killed if it exceeds that duration, and catch the timeout error to
throw or log a clear error; change the call in kepub-conversion.service.ts where
binaryPath/args are used and ensure any thrown timeout error from execFileAsync
is handled (e.g., wrap the await execFileAsync(...) in try/catch and check for
the timeout condition to return a controlled failure).
In `@server/src/modules/reader/epub/epub.service.ts`:
- Around line 353-355: Wrap the call to koboSettingsService.getSettings(userId)
in a try/catch inside the EPUB handling flow so that any exception during
settings lookup is caught and treated as "no settings" — i.e., if an error
occurs, fall back to returning file.absolutePath; otherwise preserve the
existing logic that checks settings.twoWayProgressSync and
settings.convertToKepub to decide whether to continue to KEPUB conversion.
Ensure you reference the existing symbols koboSettingsService.getSettings,
settings, twoWayProgressSync, convertToKepub, and return file.absolutePath so
the change is limited to adding error handling around the settings lookup.
---
Nitpick comments:
In `@server/src/modules/kobo/services/kepub-conversion.service.ts`:
- Line 28: The assignment to this.kepubCachePath uses a non-null assertion on
this.config.get('storage.appDataPath') which can throw at runtime; update the
constructor (or the KepubConversionService initialization) to validate and
provide a safe fallback: retrieve the value from config into a local variable
(e.g. const appDataPath = this.config.get<string>('storage.appDataPath')), if
it's missing either throw a clear Error mentioning 'storage.appDataPath' or set
a sensible default (e.g. process.cwd() or configured storage dir) before calling
join to build this.kepubCachePath, ensuring the code uses the validated
appDataPath rather than the non-null assertion.
In `@server/src/modules/kobo/services/kobo-book-identity.service.ts`:
- Around line 25-28: The ensureForBook method currently uses a non-null
assertion on identities.get(bookId) which can hide missing-identity cases;
update ensureForBook to check whether the map returned by ensureForBooks
contains bookId (e.g., using identities.has(bookId) or checking the value !==
undefined) and if missing throw a clear, explicit error (include identifiers
like bookId and userId in the message) instead of using the `!` operator so
callers get a descriptive failure; keep the method signature async
ensureForBook(userId: number, bookId: number, needsLegacyNumericRemoval:
boolean): Promise<KoboBookIdentity>.
In `@server/src/modules/reader/epub/epub.service.test.ts`:
- Around line 361-382: Add two regression tests around service.getBookInfo to
exercise the failure fallbacks: (1) simulate koboSettingsService.getSettings
rejecting and assert that getBookInfo falls back to the original EPUB path
(verify bookReadService.findFileById returned path is used and that mockStat and
mockOpenFile were called with '/books/alt.epub' and that
kepubConversionService.getKepubPath was not relied upon), and (2) simulate
kepubConversionService.getKepubPath rejecting (while getSettings resolves with
convertToKepub true) and assert getBookInfo falls back to the original EPUB path
(verify kepubConversionService.getKepubPath was called with the expected args,
then mockStat/mockOpenFile were called with '/books/alt.epub'). Ensure both
tests mock bookReadService.findFileById like the existing test and use the same
user and makeEpubArchive setup to mirror the success case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 808bdc9c-4405-4fe2-b5ea-db7930f9b8f2
📒 Files selected for processing (58)
client/public/assets/foliate/view.jsclient/src/features/book/components/detail/tabs/DetailsTab.vueclient/src/features/kobo/composables/useKoboSettings.tsclient/src/features/reader/epub/__tests__/foliate-navigation.spec.tsclient/src/features/reader/epub/composables/__tests__/useFoliate.spec.tsclient/src/features/reader/epub/composables/useFoliate.tsclient/src/features/reader/shared/composables/__tests__/useReaderProgress.spec.tsclient/src/features/reader/shared/composables/useReaderProgress.tsclient/src/features/settings/KoboSettings.vueclient/src/features/settings/KoreaderSettings.vuepackages/types/src/kobo.tsserver/src/db/migrations/0014_kobo-two-way-progress-sync.sqlserver/src/db/migrations/meta/0014_snapshot.jsonserver/src/db/migrations/meta/_journal.jsonserver/src/db/schema/kobo.tsserver/src/db/schema/reader.tsserver/src/modules/architecture/architecture-boundaries.test.tsserver/src/modules/book/book.controller.test.tsserver/src/modules/book/book.controller.tsserver/src/modules/book/book.repository.test.tsserver/src/modules/book/book.repository.tsserver/src/modules/book/book.service.test.tsserver/src/modules/book/book.service.tsserver/src/modules/book/dto/book-dto-validation.test.tsserver/src/modules/book/dto/save-progress.dto.tsserver/src/modules/kobo/dto/kobo-auth-device-body.dto.tsserver/src/modules/kobo/dto/kobo-auth-refresh-body.dto.tsserver/src/modules/kobo/dto/kobo-dto-validation.test.tsserver/src/modules/kobo/dto/update-settings.dto.tsserver/src/modules/kobo/kobo-auth.controller.test.tsserver/src/modules/kobo/kobo-auth.controller.tsserver/src/modules/kobo/kobo-device.controller.test.tsserver/src/modules/kobo/kobo-device.controller.tsserver/src/modules/kobo/kobo-sync.controller.test.tsserver/src/modules/kobo/kobo-sync.controller.tsserver/src/modules/kobo/kobo-user.controller.test.tsserver/src/modules/kobo/kobo.module.test.tsserver/src/modules/kobo/kobo.module.tsserver/src/modules/kobo/services/kepub-conversion.service.test.tsserver/src/modules/kobo/services/kepub-conversion.service.tsserver/src/modules/kobo/services/kobo-book-identity.service.test.tsserver/src/modules/kobo/services/kobo-book-identity.service.tsserver/src/modules/kobo/services/kobo-download.service.test.tsserver/src/modules/kobo/services/kobo-download.service.tsserver/src/modules/kobo/services/kobo-reading-state.service.test.tsserver/src/modules/kobo/services/kobo-reading-state.service.tsserver/src/modules/kobo/services/kobo-settings.service.test.tsserver/src/modules/kobo/services/kobo-settings.service.tsserver/src/modules/kobo/services/kobo-sync.service.test.tsserver/src/modules/kobo/services/kobo-sync.service.tsserver/src/modules/koreader/koreader.repository.test.tsserver/src/modules/koreader/koreader.repository.tsserver/src/modules/koreader/koreader.service.test.tsserver/src/modules/koreader/koreader.service.tsserver/src/modules/reader/epub/epub.module.test.tsserver/src/modules/reader/epub/epub.module.tsserver/src/modules/reader/epub/epub.service.test.tsserver/src/modules/reader/epub/epub.service.ts
💤 Files with no reviewable changes (2)
- server/src/modules/kobo/dto/kobo-auth-refresh-body.dto.ts
- server/src/modules/kobo/dto/kobo-auth-device-body.dto.ts
Preserve Kobo bookmark metadata and tolerate null auth payloads/settings failures. Cover review-reported regressions with focused tests.
|
🎉 This PR is included in version 1.9.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Closes #249
Summary by CodeRabbit
New Features
Improvements