Skip to content

fix(file): allow physical books to be moved between libraries#296

Open
dsmouse wants to merge 7 commits intogrimmory-tools:developfrom
dsmouse:issue/295
Open

fix(file): allow physical books to be moved between libraries#296
dsmouse wants to merge 7 commits intogrimmory-tools:developfrom
dsmouse:issue/295

Conversation

@dsmouse
Copy link
Copy Markdown
Contributor

@dsmouse dsmouse commented Mar 30, 2026

Fixes #295

What went wrong
FileMoveService.processSingleMove bails out early when a book has no associated files. Physical books have no BookFileEntity records by design, so they were silently dropped at this guard with no feedback to the user.

Fix
Before returning, check isPhysical. If true, skip file I/O entirely and update only the library/path association in the database, then send the usual BOOK_UPDATE websocket notification so the UI reflects the change.

Summary by CodeRabbit

  • New Features

    • Filtered shelf views now include other users' public shelves in addition to the user's own.
  • Bug Fixes

    • File-move process deduplicates file entries and avoids cleaning above library roots.
    • Physical-book moves reliably update library location and emit update notifications.
  • Tests

    • Unit tests updated for new shelf-filtering semantics and null-input handling.

github-actions bot and others added 5 commits March 21, 2026 02:17
## [2.3.0](grimmory-tools/grimmory@v2.2.6...v2.3.0) (2026-03-21)

### Features

* **release:** document develop-based stable release previews ([930e526](grimmory-tools@930e526))

### Bug Fixes

* **api:** fix potential memory leaks in file processing ([031e8ae](grimmory-tools@031e8ae))
* **ci:** correct artifact download action pin ([37ca101](grimmory-tools@37ca101))
* **ci:** publish PR test results from workflow_run ([11a76bf](grimmory-tools@11a76bf))
* **ci:** repair release preview and test result publishing ([afa5b81](grimmory-tools@afa5b81))
* drop telemetry from app ([grimmory-tools#52](grimmory-tools#52)) ([4d82cb7](grimmory-tools@4d82cb7))
* **ui:** repair frontend compile after rebrand ([fea1ec6](grimmory-tools@fea1ec6))

### Refactors

* **build:** rename frontend dist output to grimmory ([ecf388f](grimmory-tools@ecf388f))
* **i18n:** rename booklore translation keys to grimmory ([eb94afa](grimmory-tools@eb94afa))
* **metadata:** move default parser from Amazon to Goodreads ([e252122](grimmory-tools@e252122))
* pull kepubify & ffprobe during build ([grimmory-tools#50](grimmory-tools#50)) ([1c15629](grimmory-tools@1c15629))
* **ui:** rebrand frontend surfaces to grimmory ([d786dd8](grimmory-tools@d786dd8))

### Chores

* **api:** remove the custom startup banner ([98c9b1a](grimmory-tools@98c9b1a))
* **deps:** bump flatted from 3.4.1 to 3.4.2 in /booklore-ui ([grimmory-tools#73](grimmory-tools#73)) ([c4bd0c7](grimmory-tools@c4bd0c7))
* **funding:** point support links at opencollective ([55c0ac0](grimmory-tools@55c0ac0))
* **release:** 2.2.7 [skip ci] ([0b5e24c](grimmory-tools@0b5e24c))
* remove old verbose PR template, replace with temporary more low-key one. ([grimmory-tools#84](grimmory-tools#84)) ([b868526](grimmory-tools@b868526))
* **ui:** drop financial support dialog ([grimmory-tools#21](grimmory-tools#21)) ([62be6b1](grimmory-tools@62be6b1))

### Documentation

* updated supported file formats in README.md ([grimmory-tools#68](grimmory-tools#68)) ([f912e80](grimmory-tools@f912e80))

### Style

* **i18n:** normalize translation json formatting ([grimmory-tools#89](grimmory-tools#89)) ([857290d](grimmory-tools@857290d))
* **ui:** simplify the topbar logo branding ([0416d48](grimmory-tools@0416d48))
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75682434-643e-424b-b05a-8e547fc02ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 80a544b and ffceebe.

📒 Files selected for processing (1)
  • booklore-api/src/main/java/org/booklore/util/BookUtils.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • booklore-api/src/main/java/org/booklore/util/BookUtils.java

📝 Walkthrough

Walkthrough

Centralized shelf-filtering into BookUtils.filterShelvesByUserId (handles nulls and public shelves); updated services to delegate to that utility; deduplicated book-file processing and fixed physical-book library moves in FileMoveService; updated unit tests for the new shelf-filtering semantics.

Changes

Cohort / File(s) Summary
Shelf utility
booklore-api/src/main/java/org/booklore/util/BookUtils.java
Added public static Set<Shelf> filterShelvesByUserId(Set<Shelf>, Long) which returns empty set for null input and keeps shelves that are public or owned by the given user.
Services delegating to utility
booklore-api/src/main/java/org/booklore/service/book/BookService.java, booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java, booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java
Replaced inline null-check + stream filter with calls to BookUtils.filterShelvesByUserId(...), centralizing filtering behavior and removing duplicated implementations.
File move processing
booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java
Deduplicates BookFileEntity list by id (ignore null ids), uses deduped list for validation/staging/DB updates; handles physical books by updating library, clearing EM, reloading, and publishing update instead of early-return; adjusts cleanup root set construction to use HashSet and include source library root.
Unit tests
booklore-api/src/test/java/org/booklore/service/book/BookServiceTest.java
Updated tests to reflect new semantics: include user-owned and other users' public shelves, exclude other users' private shelves, and assert null input yields empty set.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant FileMoveService
  participant BookRepository
  participant EntityManager
  participant TopicPublisher

  Client->>FileMoveService: requestMove(bookId, targetLibrary)
  FileMoveService->>BookRepository: findBookWithFiles(bookId)
  alt no files && isPhysical == true
    FileMoveService->>BookRepository: updateLibrary(bookId, targetLibrary)
    FileMoveService->>EntityManager: clear()
    FileMoveService->>BookRepository: findBookWithFiles(bookId)
    FileMoveService->>TopicPublisher: publish(BOOK_UPDATE, book)
    FileMoveService->>Client: return success
  else has files
    FileMoveService->>FileMoveService: deduplicate bookFiles by id
    FileMoveService->>FileMoveService: validate source paths and plan staging/commit
    FileMoveService->>BookRepository: update filenames/subpaths as needed
    FileMoveService->>Client: return success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through utils and shelves anew,
Public nooks now visible in view,
Files pruned of doubles, paths set right,
Physical tomes now travel light,
The rabbit cheers this tidy bite!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Code changes include refactoring of filterShelvesByUserId (centralizing to BookUtils) and test updates, which extend beyond the core issue #295 scope of physical book movement. The filterShelvesByUserId refactoring should be separated into a distinct PR to maintain focus on the primary objective of issue #295 (physical book library moves).
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Issue/295 fix(file): allow physical books to be moved between libraries' follows the conventional commit format with type prefix 'fix' and descriptive subject.
Description check ✅ Passed The PR description covers the linked issue, explains the problem and fix, but lacks structured sections matching the template (no explicit 'Changes' section).
Linked Issues check ✅ Passed The PR addresses issue #295 by implementing physical book library moves: delegating filter logic to BookUtils, updating FileMoveService to skip file I/O for physical books, and sending BOOK_UPDATE notifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java (1)

125-130: ⚠️ Potential issue | 🔴 Critical

The physical-book move path is still missing.

processSingleMove() still returns as soon as bookFiles is empty, so a physical book is skipped exactly as before: no library/path update happens and no BOOK_UPDATE is sent. That is the original issue #295 behavior. This needs a DB-only branch before the early return that updates the target library/path, skips file I/O, refetches, and emits the normal notification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java`
around lines 125 - 130, processSingleMove currently returns early when
bookEntity.getBookFiles() is empty, skipping the DB-only move path; modify
processSingleMove so that if bookFiles is null or empty it performs a
database-only update: set the BookEntity's libraryId and path to the target
values, persist the change (via the repository/save used elsewhere), refetch the
updated BookEntity (to ensure relations are loaded), and emit the normal
BOOK_UPDATE notification before returning; keep the existing file-I/O code path
for non-empty bookFiles (refer to processSingleMove and the local variable
bookFiles / bookEntity.getBookFiles()) so physical files are skipped but
library/path and event behavior remain identical for empty-file (physical-book)
moves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-api/src/main/java/org/booklore/service/book/BookService.java`:
- Around line 524-528: The change in BookService.filterShelvesByUserId now
allows other users' public shelves, but BookUpdateService.buildBook and
MetadataRefreshService.updateBookMetadata still use the old owner-only predicate
causing inconsistent serialization; centralize the shelf-visibility predicate
and apply it everywhere: update BookUpdateService.buildBook and
MetadataRefreshService.updateBookMetadata (and any builders used by getBook(),
getBookDTOs(), BookFileAttachmentService.getUpdatedBook(),
BookFileDetachmentService.getUpdatedBook()) to call the centralized method
(e.g., BookService.filterShelvesByUserId or a new util like
ShelfVisibility.shouldInclude(shelf, userId)) so all code paths use the same
logic for owner-or-public shelves. Ensure you replace inline predicates with the
shared method and run tests for these service methods.

In `@booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java`:
- Line 130: The current use of Stream.distinct() on bookEntity.getBookFiles()
only removes identical object references; change both processSingleMove() (where
List<BookFileEntity> bookFiles =
bookEntity.getBookFiles().stream().distinct().toList();) and moveSingleFile() to
deduplicate by the stable identifier instead: group or map by
BookFileEntity.getId() and collect a single instance per id (e.g., use
.collect(Collectors.toMap(BookFileEntity::getId, Function.identity(),
(a,b)->a)).values() or a groupingBy then pickFirst) so subsequent staging/moving
logic operates on unique database rows rather than object instances.

---

Outside diff comments:
In `@booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java`:
- Around line 125-130: processSingleMove currently returns early when
bookEntity.getBookFiles() is empty, skipping the DB-only move path; modify
processSingleMove so that if bookFiles is null or empty it performs a
database-only update: set the BookEntity's libraryId and path to the target
values, persist the change (via the repository/save used elsewhere), refetch the
updated BookEntity (to ensure relations are loaded), and emit the normal
BOOK_UPDATE notification before returning; keep the existing file-I/O code path
for non-empty bookFiles (refer to processSingleMove and the local variable
bookFiles / bookEntity.getBookFiles()) so physical files are skipped but
library/path and event behavior remain identical for empty-file (physical-book)
moves.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2433342c-570a-4273-be1f-66e02afed094

📥 Commits

Reviewing files that changed from the base of the PR and between f1cbcf3 and 2614f23.

📒 Files selected for processing (3)
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java
  • booklore-api/src/test/java/org/booklore/service/book/BookServiceTest.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/test/java/org/booklore/service/book/BookServiceTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java
booklore-api/src/test/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

Prefer focused unit tests; use @SpringBootTest only when the Spring context is required

Files:

  • booklore-api/src/test/java/org/booklore/service/book/BookServiceTest.java
🧠 Learnings (3)
📚 Learning: 2026-03-26T01:46:48.863Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T01:46:48.863Z
Learning: Applies to booklore-api/src/test/**/*.java : Prefer focused unit tests; use `SpringBootTest` only when the Spring context is required

Applied to files:

  • booklore-api/src/test/java/org/booklore/service/book/BookServiceTest.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
📚 Learning: 2026-03-26T01:46:48.863Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T01:46:48.863Z
Learning: Applies to booklore-api/src/**/*.java : Use MapStruct for entity/DTO mapping

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java

@dsmouse dsmouse marked this pull request as draft March 30, 2026 23:42
@dsmouse dsmouse marked this pull request as ready for review March 31, 2026 00:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-api/src/main/java/org/booklore/util/BookUtils.java`:
- Around line 16-21: The filterShelvesByUserId method in BookUtils can NPE when
userId is null because the predicate calls userId.equals(...); change the
predicate to avoid calling methods on userId — e.g. replace the filter with
Objects.equals(userId, shelf.getUserId()) || shelf.isPublicShelf() (or check
shelf.isPublicShelf() first) so public shelves are always included and null
userId is handled safely; update imports if using java.util.Objects.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0836e7a1-92e7-458a-a233-c59982cb6bcc

📥 Commits

Reviewing files that changed from the base of the PR and between 2614f23 and 80a544b.

📒 Files selected for processing (5)
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java
  • booklore-api/src/main/java/org/booklore/util/BookUtils.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/file/FileMoveService.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java
  • booklore-api/src/main/java/org/booklore/util/BookUtils.java
🧠 Learnings (2)
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java
  • booklore-api/src/main/java/org/booklore/util/BookUtils.java
📚 Learning: 2026-03-26T01:46:48.863Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T01:46:48.863Z
Learning: Applies to booklore-api/src/**/*.java : Use MapStruct for entity/DTO mapping

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
🔇 Additional comments (2)
booklore-api/src/main/java/org/booklore/service/metadata/MetadataRefreshService.java (1)

850-852: Good consolidation to shared shelf-filter utility.

The delegation keeps filtering semantics centralized and avoids drift across services.

booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java (1)

363-365: Nice refactor to remove duplicated shelf-filtering logic.

This keeps behavior consistent with other services and simplifies maintenance.

@dsmouse dsmouse marked this pull request as draft March 31, 2026 01:04
@dsmouse
Copy link
Copy Markdown
Contributor Author

dsmouse commented Mar 31, 2026

gonna put in draft to run full backend unit test

@dsmouse
Copy link
Copy Markdown
Contributor Author

dsmouse commented Mar 31, 2026

Rar5FallbackIntegrationTest > readerService_streamsImageFromRar5(Path) FAILED
org.booklore.exception.APIException at Rar5FallbackIntegrationTest.java:103

Rar5FallbackIntegrationTest > readerService_listsImagePagesFromRar5(Path) FAILED
org.booklore.exception.APIException at Rar5FallbackIntegrationTest.java:80

I didn't touch rar5 handling; unrelated failure.

@dsmouse dsmouse marked this pull request as ready for review March 31, 2026 01:26
@balazs-szucs
Copy link
Copy Markdown
Member

Rar5FallbackIntegrationTest > readerService_streamsImageFromRar5(Path) FAILED org.booklore.exception.APIException at Rar5FallbackIntegrationTest.java:103

Rar5FallbackIntegrationTest > readerService_listsImagePagesFromRar5(Path) FAILED org.booklore.exception.APIException at Rar5FallbackIntegrationTest.java:80

I didn't touch rar5 handling; unrelated failure.

agreed, that should be fine.

Thanks for this I'll try to review it, later.

@balazs-szucs
Copy link
Copy Markdown
Member

Those RAR test failures are most likely caused by a missing unarchiving package in your PATH that supports the RAR format.

The Docker container includes the RAR binary, which is why the tests pass there but fail locally. There's an open PR that, among other things, will make those tests conditional on whether the user has RAR tooling available in their PATH. This way, the tests simply won't run instead of failing, avoiding further confusion.

Anyways, this is to say, in you want to fix it, download unrar, otherwise if you are not interested in development of CBX specific code you can ignore that.

Thanks again for your PRs again!

@imajes imajes changed the title Issue/295 fix(file): allow physical books to be moved between libraries fix(file): allow physical books to be moved between libraries Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Physical books cannot be moved between libraries

2 participants