* feat(storygraph): add StoryGraph sync integration
Sync reading status and progress from BookOrbit to StoryGraph using
session-cookie authentication. Includes book matching with manual
rematch and edition selection, per-user sync toggles, a throttled
request queue, a storygraph_sync permission, and a StoryGraph
settings page.
Closes#274
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi6e8ZJRAGMmRSmSj9sWXR
* fix(storygraph): address automated review findings
- clear stale cookie validation badge when inputs change
- reject saves with only one cookie value instead of silently dropping it
- handle linked-books load failures with a retryable error state so post-action
reloads cannot misattribute failures to the action
- add a 30s request timeout so a hung StoryGraph response cannot stall a user's
sync queue indefinitely
- reserve throttle slots synchronously so concurrent requests serialize
- reject empty cookie strings that would overwrite stored credentials
- report the retry response status when the rereading fallback fails
- add component specs for the connection card and linked books, plus regression
tests for the throttle, settings, and retry-status fixes
* test(storygraph): fully restore setTimeout spies between queue tests
* feat(storygraph): add sync-scope option
Adds the book sync scope selector (all eligible books vs selected books
only) and the per-book StoryGraph sync control on the book details page,
mirroring the Hardcover integration's controls. All books with a syncable
status are now eligible — the previous connected-at cutoff that skipped
books finished before connecting is removed in favor of explicit scope
and per-book selection.
* fix(storygraph): stream live sync progress and surface failed books
The manual sync progress stream emitted the same mutated status object,
so distinctUntilChanged dropped every update after the first and the bar
never moved. Progress snapshots are now immutable and track processed
books (synced + skipped + failed), so the bar advances for every book.
Adds live synced/skipped/failed counters, a persistent last-run summary
from a terminal completed/cancelled event, and a failed-books list under
Manual sync backed by GET /storygraph/sync/failures. Details-page sync
state now refetches when the global sync scope changes.
(cherry picked from commit 075eeaa0c8678d4ebbba5dcce7aa15df42d64f51)
* feat(storygraph): show distinguishing identifiers in the editions list
Edition rows previously showed only format, pages, and language, making
identical-looking editions impossible to tell apart. The editions parser
now also scrapes ISBN, publisher, publication date, and cover image when
present, and each row shows the edition title, those identifiers, a cover
thumbnail, and the StoryGraph edition id linking to the edition page for
verification before switching.
(cherry picked from commit 09ec111613e9be149b556f64eb242cd3d82b296c)
* fix(storygraph): auto-refresh linked books after sync and add unmatched hint
The Linked books list only loaded on mount, so a book linked during a manual
sync run stayed on 'Not linked yet' until a manual page refresh. It now
silently reloads when a run completes (no spinner flash, keeps expanded rows),
guarded by a request id so overlapping reloads can't resolve out of order.
Books StoryGraph couldn't match automatically now show a short hint pointing
at 'Try auto-match' or pasting the StoryGraph URL.
(cherry picked from commit 1d0cfa1e06f3e211258620f7ad5e61373df7a310)
* feat(storygraph): compact edition link and wrap identifiers on mobile
The editions list showed the raw StoryGraph edition UUID as a long
monospace hyperlink, which was hard to read and cluttered the row. Replace
it with a small icon-only 'Open on StoryGraph' button (aria-labelled) next
to 'Use this', and let the publisher/date/ISBN line wrap instead of
truncating so the ISBN is no longer cut off on narrow/mobile screens.
(cherry picked from commit cdf4d64063534adeea9daafad11cd58a0cbc9a00)
* fix(storygraph): re-sync books re-added after being marked unread, clarify labels
A book synced while 'read' then marked 'unread' left its synced markers in
place, so re-marking it read compared against a stale lastSyncedStatus and was
silently skipped (shown as Linked but never re-pushed) until 'Try auto-match'.
On the unread transition the synced markers are now cleared while keeping the
match, so re-adding the book re-syncs.
Also present match methods with real casing (ISBN/Title/Cached/Manual) and
clarify that the manual-link field takes a StoryGraph URL or book ID (the code
after /books/), not an ISBN.
(cherry picked from commit 771e7e4fe793afbc30800717ae1c017ca307e5e1)
* refactor(storygraph): compare skippedBooks explicitly in sync-status dedup
The active-sync stream's equality check omitted skippedBooks, relying on
processedBooks (synced+skipped+failed) to capture skip-only changes. That is
correct but non-obvious; comparing every counter explicitly makes the intent
clear. No behavior change.
(cherry picked from commit f267088939f0d2493caadab8eb90f24ab4b28c0b)
* fix(storygraph): encode manually-entered book id in the lookup path
The manual-link id can be pasted directly, so a stray ?, #, or / would change
which /books/ URL is requested. Encode it before interpolating; a bad id now
404s cleanly instead of being stored.
* style(storygraph): fix prettier formatting and remove em dashes
The CI Lint (Client) job runs prettier --check, which flagged
StorygraphLinkedBooks.vue and StorygraphSyncProgress.vue. Reflow both
to prettier style and replace em dashes in UI strings and comments with
regular punctuation per project convention.
* fix(storygraph): harden sync access and batching
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
* feat(types): add readwise_sync permission
* feat(types): add readwise settings types
* feat(db): add readwise_user_settings table
* feat(readwise): add constants
* feat(readwise): add throttle queue
* feat(readwise): add REST client
* feat(readwise): add repository
* feat(readwise): add settings service
* feat(readwise): add sync service with watermark
* refactor(readwise): drop unused logger in settings service
* feat(readwise): add debounced scheduler
* feat(readwise): add event listener
* feat(readwise): add controller and dtos
* feat(readwise): wire module into app
* test(readwise): add readwise_sync to authorization matrix
* feat(annotation): emit annotation.created on device sync ingest
* feat(client): add readwise api client
* feat(client): add useReadwiseSettings composable
* feat(client): add Readwise settings page
* feat(client): add Readwise settings route
* feat(client): add Readwise to settings nav
* fix(readwise): avoid typeof this in settings type annotation
Production build (nest build → tsc via tsconfig.build.json) rejects
`Parameters<typeof this.repo.upsertSettings>[1]` with TS2304. Reference
the repository class type directly so the prod image compiles.
* fix(client): add Readwise sync to admin permission list
The admin UserFormDrawer renders a hardcoded permission list, not an
iteration of PERMISSION_LABELS, so readwise_sync was ungrantable via UI.
Add it to the Devices & Access group and the Standard preset, mirroring
Hardcover sync.
* feat(readwise): send book cover to Readwise via OpenLibrary ISBN
Populate the Readwise highlight image_url with the OpenLibrary cover URL
built from the book's isbn13 (falling back to isbn10). Uses ?default=false
so a missing cover yields 404 (no image) rather than a blank placeholder.
Books without an ISBN send no image_url. BookOrbit's own cover endpoint is
auth-gated and stores no public source URL, so a public third-party cover
URL is the zero-infra way to give Readwise an image.
* test(readwise): add type parameters to vi.fn mocks
oxlint (vitest/require-mock-type-parameters) fails on untyped vi.fn()
in the settings page test; the pre-commit hook only runs eslint, so
this surfaced under full lint:check.
* docs(readme): add Readwise Sync to features list
* fix(readwise): surface settings fetch errors in the UI
The settings page destructured error from useReadwiseSettings but never
rendered it. A failed fetch on mount left the user with an empty default
form and no indication that loading failed. Render an error banner when
error is set, and cover it with component tests.
* test(readwise): cover settings page token test, save, and visibility toggle
Adds component tests for the Test-connection flow (empty/valid/invalid
token), Save (success clears the field, failure surfaces an error toast),
and the show/hide token toggle — the interactive handlers Codecov flagged
as uncovered in ReadwiseSettings.vue.
* test(readwise): match settings buttons by exact label in test helper
Use an exact text match instead of substring matching in the testButton
helper so it can never return the wrong button if labels overlap.
* fix(readwise): harden highlight sync
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Oscar Nogueira Neto <oscarnogueira@gmail.com>
Allow smart scopes to opt into Kobo sync while preserving collection sync behavior. Generate the current Drizzle migration and keep smart-scope matching aligned with the normal book query path.
Fixes#202
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Jane Doe <janedoe@users.noreply.github.com>
Introduce a LIBRARY_BROWSE_ROOT environment variable (defaulting to the
current directory) to restrict directories users can browse and select when
setting up library paths. Add a PathPolicyService on the backend to enforce
path containment checks securely, and update the frontend FolderPickerModal
to prevent navigating above the configured browse root.
Closes#567
Truncate long path segments generated during upload pattern resolution and
download filename generation to prevent ENAMETOOLONG errors. Keep the file
extension and suffix of the segment, and use a deterministic hash of the
original segment to ensure unique truncations.
Closes#549
Add support for user-scoped personal notes (reviews) and allow clearing book ratings to null,
providing full sync capabilities for these actions in the KOReader plugin. Modularize the
KOReader plugin codebase into dedicated mixin files to improve maintainability and expand
the catalog detail view with related sections (series, author, similar books).
Closes#552
Add configuration settings and backend support to define default library access permissions for newly created users. Newly registered users automatically receive these default library access permissions upon creation. Also align current users page controls and magic link options in the admin tab settings UI, adding styled callout cards for empty states.
Closes#486
Records success and failure events for Kobo device syncs in the database, including operation duration, event types, and error details. Surfaces the recent sync history in the Kobo Settings UI for users to monitor their device activity.
Introduces the koreader_unmatched_book_devices schema to track unmatched
books per device. Adds API endpoints and UI in settings for dismissing
unmatched books and completely removing a KOReader device.
Closes#504
When KOReader syncs progress or annotations, it identifies books by a document
hash. BookOrbit normally resolves this automatically, but some books cannot be
matched. This adds a tracking table for unmatched hashes and a UI to manually
link them to existing library books.
Closes#458
During an automatic metadata refresh, the fetched Hardcover edition ID was not being copied to the update payload. This ensures the value is persisted to the database alongside the other metadata fields.
Closes#440
Adds a fixedLayoutSpread preference to EPUB reader settings to allow forcing single-page display. This is particularly useful for image-based EPUBs like comics and manga.
Closes#487
Reading source is often less relevant for day-to-day analytics than
book format or reading pace. Shift the source distribution chart to the
end of the user dashboard and change the default breakdown dimension
from source to format to prioritize more useful metrics. Include a
migration to update existing users who have not customized their chart
order.
The group rule validator now accepts an optional `provider` field for
`communityRating` rules. This enables smart scopes to filter books based
on community ratings from specific providers (e.g., Goodreads, Amazon)
rather than just the aggregate rating.
The validator ensures that the provider field is only applied to community
rating rules and rejects unsupported providers.
Closes#477
Adds support for displaying custom metadata as columns in the book
table view. The frontend now fetches active fields and dynamically
renders the corresponding cells, backed by book card API updates.
Closes#465
Previously, the explicit metadata search modal for a book was restricted
to only the providers configured in the library's field rules. This
prevented users from manually searching a specific provider if it
was not part of their default automated fetch pipeline.
This change allows all globally enabled providers to be used during
a manual metadata search, while still defaulting the selected UI
checkboxes to the providers specified in the field rules.
The KOReader plugin previously matched series by name, which caused
issues for series with identical names or special characters. This
updates the plugin and backend to use the exact series ID for
navigation and filtering, ensuring all series load reliably. Also
bumps the KOReader plugin version to 1.0.1.
Closes#466
Add full-stack support for uploading, managing, and using custom SVG icons for libraries and smart scopes. Includes database schema, server module for storage and sanitization, and client UI under Appearance settings.
Add the KOReader catalog surfaces, plugin package metadata, and device progress handling needed for the plugin workflow. Keep progress status updates non-blocking so secondary status failures do not reject successful sync writes.
Users rely on community feedback when evaluating books in their library. This change
introduces aggregated community ratings and rating counts from external metadata
providers, allowing users to see popular opinion directly within the book details
without needing to leave the application.
Closes#414
Adds hardcoverEditionId to the database schema and shared types, enabling it to be stored as first-class metadata. Updates the file metadata parsers and writers (EPUB, PDF, CBZ) to read and embed this ID, and exposes it in the frontend metadata editor.
Closes#420
Allow users to define new custom fields for books beyond the standard
metadata properties. This provides flexibility for cataloging arbitrary
data like reading formats, editions, or personal ratings.
Closes#419
Both AddToCollection and SendBook previously only operated on the explicitly provided list of book IDs. This prevented them from working correctly when a user selected all items across multiple pages in the UI. They now inherit from BulkSelectionDto and resolve queries to apply to all matching books.
Closes#416
Introduce a per-book sync toggle in the book detail view that lets
users include or exclude individual books from Hardcover sync.
Add a book sync scope setting with two modes:
- all_eligible: sync all books unless explicitly excluded (default)
- selected_only: sync only books the user explicitly includes
Extract sync eligibility logic into hardcover-sync-policy.ts for
testability. The toggle handles unread books correctly: sync is skipped
while the book is unread, but the user's inclusion intent is stored so
sync activates automatically once reading starts.
Closes#412
Add two per-file override flags to the Book Dock finalize flow:
- skipDuplicateCheck: bypasses ISBN/title+author duplicate detection for
a specific file. In book_per_folder mode the file naturally lands in
the same folder as the existing book, so createBookRecord auto-attaches
it as a second format on the same record.
- targetFileName: lets the user rename the file at the destination to
resolve a same-path collision. The extension is always appended from
the file format; if the user includes the extension it is stripped
first to avoid doubling. Setting targetFileName also implicitly skips
the duplicate check. A directory-traversal guard rejects any name
whose resolved path escapes the intended destination folder.
Frontend: results panel shows an "Import anyway" button for isDuplicate
rows and an inline rename input for file-exists-at-target rows. Both
retry calls forward defaultLibraryId/defaultFolderId so files that rely
on dialog-level defaults get the right destination. Concurrent double-
click is guarded. reimportingIds uses reactive(Set) so spinner and
disabled states update correctly.
Closes#213
Ensure the settings UI saves inherit toggles immediately and guards eligible-count previews against stale async responses. Align library override DTO validation with the shared partial override contract, and record manual library runs even when nothing queues.
Extract and map multiple series memberships from AudNexus (primary, secondary, and fallback series) to a new seriesMemberships array. Update Goodreads mapper tests to verify correct extraction of the first series.
Closes#383
Add a preview-and-review workflow that matches Hardcover books by cached IDs, ISBNs, slugs, and title/author confidence before updating BookOrbit read status.
Import reading progress only when requested and safe, preserve existing local progress, and keep status imports resilient when progress writes fail.
Closes#394
Emit transfer events when watcher reconciliation moves a book between libraries. Keep file-as-book and folder-as-book watcher paths consistent across Docker, direct runs, and delayed copy-delete moves.
Closes#386
Expose zero-book first-class metadata entities in browse so admins can review orphaned rows before deleting them.
Keep inline metadata values out of empty filtering because they only exist through book metadata rows.
Closes#370
Update cover mutation paths to bump the book timestamp used for cover cache busting, and carry per-book collapsed-series cover versions so child covers do not reuse a representative book timestamp.
Introduces a new feature to fetch, parse, and display app release notes
and update highlights directly within the UI.
- Backend: Adds the `release-notes` module to fetch GitHub releases
and parse markdown highlight sections.
- Frontend: Adds the `WhatsNew` dialog and view, including custom
markdown rendering and an image lightbox for update media.
- Tools: Adds validation scripts (`whats-new-lint`, `whats-new-preview`)
to ensure release content parses correctly before publication.
* feat(metadata): add Aladin Korean book metadata provider
Add support for fetching Korean book metadata from Aladin OpenAPI.
Includes search by title/author/ISBN and lookup by ISBN-10/13.
- Add ALADIN to MetadataProviderKey in @bookorbit/types
- Add AladinProviderConfig to ProviderConfigurations
- Create AladinProvider with IdentifiableProvider interface
- Add mapper for Aladin API response to MetadataCandidate
- Register provider in MetadataFetchModule
- Add provider config merge, validation, and connection test
- Include 24 unit tests (provider + mapper)
* feat(client): add Aladin provider UI integration
* refactor(metadata): use 'Aladin' label and favicon-based icon
* feat(metadata): complete Aladin provider end-to-end wiring
Persist the Aladin ItemId across fetch, scoring, locks, and file metadata round-trip, and update the affected tests.
Co-authored-by: Kyungwook Park <288273+kyungw00k@users.noreply.github.com>
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Kyungwook Park <288273+kyungw00k@users.noreply.github.com>
This introduces a many-to-many relationship between books and series, resolving the previous limitation where a book could only belong to a single series. It updates the database schema, query builders, server DTOs, and the Vue client interface to manage multiple series memberships per book.
Closes#353
Adds a display preference to conditionally render book spines on comic
formats. The new setting is defaulted to false. Various book cover
surfaces across the application now respect this preference by checking
if the format is a comic before rendering the spine.
Closes#295
Adds the isUpNext operator to seriesStatus allowing books to be filtered by whether they are the next unread book in a series where the previous book is completed.
Closes#321
Introduce devices and rating evaluators backed by a new reading_sessions.source
column, so badges can reward multi-device reading and book ratings. Wire the
source through Kobo and reading-session services and seed the new badges.
Introduces PathService.createDirectory with comprehensive path validation
to prevent directory traversal and symlink attacks. It validates parent paths
against a blocked list, ensures the parent is a real directory, and blocks
unsafe characters in folder names.
Filesystem errors like EEXIST and EACCES are mapped to standard HTTP
exceptions to provide clear feedback to the client.
Closes#332
* feat(metadata): add LubimyCzytac metadata provider
- Add LubimyCzytac scraper and provider implementation
- Add lubimyczytacId field across types, schema, extractors, and metadata service
- Register lubimyczytac provider in module, pipeline, repository, and config service
- Add provider icon, color, link, and UI support in book detail views
- Add migration for lubimyczytac_id column
Closes#304
* fix(books): keep cover card read button opening reader when thumbnail click prefers details
The overlay primary button should always open the reader regardless of
the thumbnailClickAction setting. That setting only controls card click
behaviour.
* fix(books): add explicit read button to hover overlay when thumbnail click prefers details
When thumbnailClickAction is 'details', a dedicated Read button now
appears in the overlay top row so users can still open the reader
directly without changing their preference.
The book dock metadata service now routes audio formats through the audio extractor to extract chapters, duration, narrators, and other embedded tags. This ensures that audiobook metadata is correctly mapped and persisted during the import process.
Closes#322
The default upload file naming patterns for single file and folder per book previously relied exclusively on the title. If multiple files lacked a title, they would map to the same path. This changes the default patterns to fall back to the original filename if the title is missing, preventing files without metadata from clumping.
Closes#308
Add server-backed jump buckets so large library, collection, and smart-scope listings can jump directly to sorted sections without loading every row up front.
Closes#163
The metadata fetch pipeline now gathers and returns detailed diagnostics
about provider selection, candidate results, and the reason for empty
resolved fields (e.g. no active providers, throttled providers, or no
candidates). These diagnostics are logged to improve observability and
make debugging metadata fetch failures significantly easier.
KOReader progress updates were updating the database but failing to
emit the progress changed event, causing Hardcover auto-sync to be
skipped. This change adds the missing event emission to the KOReader
service so that external syncs trigger correctly, and patches a query
filter issue in the Hardcover repository.
Closes#258
Add Kobo as a first-class metadata source so books can keep Kobo IDs across fetch, edit, import, scoring, and file-write flows. Provide the Python cloudscraper setup needed for production fetches while keeping local setup explicit.
Closes#247
* feat(kobo): support two-way progress sync
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
* fix(settings): restore thumbnail click preference
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.
* fix(kobo): harden two-way progress sync fallbacks
Preserve Kobo bookmark metadata and tolerate null auth payloads/settings failures.
Cover review-reported regressions with focused tests.