The genre blocklist allows users to define a global list of strings
that are prevented from being applied to a books genre field during
metadata fetches. Case-insensitive filtering executes directly inside
the metadata pipeline before merge strategies run, preventing unwanted
values from persisting across providers.
Closes#235
Add dedicated dashboard scrollers for audiobooks and want-to-read lists to improve content discoverability. Previously, audiobook progress was merged with reading progress, making it difficult to find active audiobooks. Separate the queries to ensure audiobooks are handled exclusively by the new listening scroller and expose the user's want-to-read queue directly on the dashboard for easier access.
Introduces a new 'stack' cover mode for series cards, rendering the covers
as a stacked pile. This mode has also been made the default for series card
display preferences. Settings UI is updated to allow users to toggle this
mode alongside existing mosaic and single-cover options.
Closes#223
Preserve RanobeDB IDs through write-back, file import, and load-from-file so saved metadata survives round trips.
Expose saved RanobeDB sources in the book detail surfaces with the provider icon and database link.
Closes#207Closes#210
- Extend file write pipeline to write metadata fields directly into audiobook files.
- Map metadata fields to standard audio tag mappings and run ffmpeg to embed tags and cover art for M4B, M4A, MP3, and FLAC files.
- Update UI in edit metadata tab to show file write status and highlight written fields.
Handle edge cases in metadata extraction, file write-back status checks, and comic XML building. This ensures narrators are not incorrectly populated when they match the author/artist, preserves audibleId during chapter extraction, prevents whitespace issue numbers from breaking the XML, and improves the UI to clearly show why write-back is disabled.
Transition from title-based to ID-based routing for series to prevent
404 errors when a series has an excessively long title. This introduces
numeric IDs for series, backed by schema updates, and updates the
frontend router to use seriesId instead of seriesName.
Closes#198
* feat(audio): embed cover image in audiobook files
- Add audio cover embedder
- Implement format writers for M4B, M4A, MP3, and FLAC using ffmpeg
- Add library settings for audio file writes
Closes#145
* fix(library): disable audio file write by default and add ffmpeg timeout
Change the default value of file_write_audio_enabled from true to false in the database schema and migrations. Automatically disable audio writing in the UI if cover writing is toggled off. Add a 60-second timeout to the ffmpeg child process to prevent hanging when embedding covers.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
* feat(metadata): add RanobeDB metadata provider
Introduces a new metadata provider to fetch book metadata from RanobeDB.
- Adds `ranobedb.client` to interact with the RanobeDB API.
- Implements `ranobedb.mapper` to map RanobeDB data to the internal metadata schema.
- Maps native Japanese titles to subtitles.
- Uses the original language as a fallback for the language field.
- Parses specific release dates to determine the original publication year.
- Registers RanobeDB as an available provider in the preferences UI.
- Adds `ranobedb_id` to the metadata database schema.
- Removes the `bookId` override when manually performing a metadata search in the UI.
Closes#114
Add a user preference to configure the label displayed underneath book
and series cards in grid view. The option allows users to select
between hiding labels completely, showing book titles, or showing series
titles.
Closes#159
Allow readers to choose which representative series cover appears in collapsed grid view so browsing can prioritize first, latest, or first unread volumes.
Closes#160
Allow users to choose how cover artwork is fitted so mixed aspect ratios render cleanly in cards, lists, carousels, and widgets.
Persist display preferences through shared types and user-preferences endpoints so these settings stay consistent across sessions and devices.
Closes#152
Introduce a user-preferences module and database schema to sync theme
settings (theme, accent, radius, background, brightness) across devices.
Users can now enable theme sync to persist their customization choices.
Closes#146
Add a dialog to batch-edit metadata for multiple selected books.
Support add/remove/replace/clear modes for list fields, with clear as an explicit
frontend-only mode.
Fix scalar input border rendering, year field validation/coercion, tags replace
optimistic updates, and submit error handling.
Closes#150
allow users to trigger file write-back and rename from the edit metadata tab as a single explicit action.
return write and rename outcomes with library setting flags so the UI can show clear result and warning states.
Closes#143
Align recommendation data and rendering with per-book media format semantics.
Expose audiobook state from recommendation queries and render audiobook cards as square while preserving portrait and spine treatment for non-audiobooks.
Closes#149
Add UI and backend support for testing metadata provider credentials
before saving. Include parsing and validation for Bearer tokens,
display test results in the settings panel, and execute a test
connection specifically against the Hardcover provider.
Closes#127
* feat(library): add bulk rename based on file naming settings
Provides a way to rename all books in a library according to the user's
configured file naming template without requiring manual metadata edits.
Includes a streaming endpoint to report progress back to the frontend.
Closes#121
* test(library): add e2e tests for bulk file rename
Introduce a comprehensive E2E test suite for the bulk rename functionality to verify behavior across different library modes (book per file, book per folder) and handle edge cases like naming collisions or special characters.
Additionally fix a TypeScript compilation error in BulkRenameView.vue by replacing `replaceAll` with a regex-based `replace` for broader target compatibility.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Prevent unauthorized users from seeing integrations they cannot use and keep settings routing consistent with API authorization behavior. Add hardcover_sync and enforce it server-side so tab visibility and endpoint access stay aligned.
Book titles and author names can contain characters that are invalid on Windows,
SMB/NFS shares, or cloud sync platforms (e.g., `:`, `?`, `*`, `"`). Without
sanitization these characters appear verbatim in generated file and folder names,
which causes failures on Windows filesystems, broken NFS/SMB mounts, and sync issues.
Add a toggleable app setting that, when enabled, sanitizes illegal filesystem
characters in all pattern-resolved paths (upload, rename, book-dock finalize, and
download). Each token value is sanitized before substitution: invalid chars are
replaced with `_`, trailing dots and spaces are stripped, and Windows reserved names
(CON, AUX, NUL, COM1-COM9, LPT1-LPT9) get a suffix to avoid conflicts. The setting
defaults to off to preserve existing behavior.
Closes#103
* feat(users): tag/genre content filters for per-user access control
Add granular per-user content filtering by tags and genres, allowing admins to set include/exclude filter rules that are enforced across all book-serving surfaces.
- Implement DB schema, repository, and SQL helpers for content filters
- Wire enforcement across core domains (Books, Collections, OPDS, Kobo sync)
- Add REST endpoints for managing filter rules
- Redesign UserFormDrawer to support configuring content restrictions
- Add read-only Restrictions tab in account settings and user list badges
- Add comprehensive unit tests and fix existing fixtures
Closes#68
* test(client): update account tab expectations for restrictions
Add file rename feature that renames physical book files on disk when
naming-relevant metadata fields are updated (title, authors, series,
seriesIndex, publishedYear).
Closes#69
Add per-user startedAt and finishedAt filtering, sorting, and timezone-safe date normalization.
Surface compact read-date editing in Details and return canonical date-only status payloads.
Preserve manual dates across status updates and reject null status patches.
Closes#66
* feat(docker): expose Book Dock folder as a configurable drop zone
Add support for an external book drop folder that users can bind-mount
into the container via the BOOK_DROP_HOST_PATH env var. Files copied or
moved into this folder are automatically picked up and ingested by the
existing Book Dock file watcher.
- docker-compose.yml: add BOOK_DROP_HOST_PATH volume override so the
book-dock path is separately bind-mountable
- .env.example: document BOOK_DROP_HOST_PATH with an example NAS path
- AppInfoResponse: add bookDockPath so the UI can show the
container-internal path admins need to bind-mount
- AppInfoService: compute and return bookDockPath from storage config
- BookDockSettings: display drop folder path with BOOK_DROP_HOST_PATH hint
- useAppInfo: expose bookDockPath ref from the app-info API response
- README: document the optional drop folder in the quickstart section
When BOOK_DROP_HOST_PATH is unset, the volume defaults to
./data/app/book-dock, preserving full backward compatibility.
No watcher code changes required - the watcher already watches
the correct container-internal path.
Closes#58
* fix(book-dock): resolve symlink paths so rescan stays idempotent
BookDockWatcherService and BookDockIngestService were building
bookDockPath with path.join(), which preserves symlinks. `@parcel/watcher`
resolves symlinks internally when reporting events (using realpath), so the
absolutePath values stored by watcher events and the paths computed by
walkAndIngest during rescan never matched. This caused findByAbsolutePath
to always return undefined on rescan, creating duplicate DB records for
every file already tracked via a watcher event.
Fix: both services now call `realpath(bookDockPath)` in
onApplicationBootstrap (after mkdir), updating the stored path to the
canonical real path that the watcher also uses. Rescan is now idempotent
regardless of symlinks in the data directory.
Verified by E2E test: 3 consecutive rescans produce zero new records.
- Add tap-to-cycle footer modes (pages, time/session, chapter) along with elapsed reading time and chapter progress on the slider
- Prevent screen dimming during reading using the Wake Lock API
- Add keyboard shortcuts (t/s/b/f/Esc/?/Home/End) with an integrated help modal
- Add go-to-location support (page number or percentage) and a resume-reading toast on book open
- add achievement schema, migrations, evaluators, APIs, and backfill support on the server
- add achievements UI, composables, toast utilities, and tests on the client
- add shared achievement/auth/notification type updates
- Replace music-metadata and mediainfo with @ffprobe-installer for all audio metadata extraction; remove mediainfo dependency
- Show BookCoverPlaceholder in all missing-cover locations: Similar/Series/Author Books carousel, cover editor panel, metadata search result cards, and metadata diff panel
- Extend BookRecommendation and SeriesBookRecommendation shared types with hasCover and authors; update recommendation and book repositories to supply them via parallel queries
- Fix cover refresh not updating in grid: BookCoverCard now merges refreshed BookDetail into the local BookCard and emits update:book; VirtualBookGrid forwards the event; Home, Series, Author, and Collection views wire the handler so covers update without a page reload
- Add audiobook covers checkbox to the Online Search drawer so users can toggle between square and portrait cover searches; result thumbnails and loading skeletons adapt their aspect ratio to match the selection
Replace the live self-join scan (O(N^2), times out at 100K+ authors) with a
background-computed candidates table served instantly at read time.
Key changes:
- Add entity_duplicate_candidates and entity_duplicate_scan_status tables
- Background job batches 1K authors at a time (~58s total for 200K authors)
- Scan endpoint reads from the indexed table (<10ms regardless of scale)
- Candidates are invalidated after merge/delete, not recomputed from scratch
- Add GET /duplicates/status and POST /duplicates/refresh endpoints
- Frontend updates clusters locally after merge/dismiss (no re-scan)
- Status bar shows last computed time and recompute button with progress
- Author browse in entity manager now shows all authors, not just those with books
- Remove author insights and duplicate suggestions panels, composables, DTOs, and all backend support
- Add genre and tag duplicate detection, merge, and dismiss with server-side paginated scan results
- Consolidate per-entity-type tools into a single entity-manager module backed by the Strategy pattern
- Support browse, merge, rename, delete, split, and dismiss across all entity types (author, genre, tag, narrator, publisher, language, series)
- Replace old tools module with unified entity-manager controller, service, and repository; add 85+ unit tests
Core performance overhaul - new library_dir_scan_state schema table persists per-directory
mtimes so unchanged directories are skipped entirely on subsequent scans, reducing filesystem
stat() calls from O(all-files) to O(changed-dirs) for large libraries.
Incremental scan (walk.ts / scanner.service.ts):
- New library_dir_scan_state table (bigint mtime, FK to library_folders ON DELETE CASCADE)
- findBookCandidates / findLooseFileCandidates now accept knownDirMtimes and return WalkResult
(candidates + skippedDirs + unchangedDirs + dirMtimes) instead of bare BookCandidate[]
- Per-dir mtime skip with 3 safety rules: never skip disc dirs, never skip dirs with disc
subdirs, never skip non-root dirs whose parent mtime changed (guards stem-named merging)
- Root directory now also skippable - parent-mtime check conditionally skipped for root since
root-level files are never stem-merged (explicitly excluded in buildBookCandidates)
- Settings hash invalidation: SHA-256 of {allowedFormats, formatPriority, excludePatterns,
organizationMode} forces full scan when settings change or on first scan after server restart
- isInUnchangedDir guard prevents false book-missing marks for skipped dirs
- Math.round() on stat().mtimeMs before storing - fixes bigint rejection of macOS fractional
milliseconds (e.g. 1776138191093.9556) in PostgreSQL
Walk improvements:
- Bounded concurrency: subdirs processed in chunks of 50 to avoid EMFILE on deep trees
- format + role cached on FileStat to avoid re-classifying the same file twice
- statFilesIntoAcc helper extracted to deduplicate parallel stat logic
- skippedDirs set propagated up from permission-denied dirs
ScanJobStore:
- acquireStartLock / releaseStartLock prevent concurrent scan starts for the same library
- markPendingRescan / consumePendingRescan queue a follow-up scan if one arrives while running
waitForStability:
- Accepts optional knownMtimeMs to skip a redundant stat() call when mtime is already known
File event processor (Issue 25, 11, 20):
- Own-book restore: check file's own book before searching all missing books, fixing false
book-moved results when an editor saves with a new inode (atomic-save pattern)
- Settings cache per library avoids repeated DB lookups during reconcile of many missing books
- Partial move guard: verify old file is gone before reassigning inode (multi-file books)
Live notifications (scanner.service.ts / file-watcher.service.ts):
- scanBookFolder now emits scan:books:added via existing WebSocket infrastructure so the
frontend grid updates live when the file watcher adds a book (all 3 code paths covered)
- bufferWatcherNotification() with 30s debounce fires one ScanCompleted notification per
library summarising watcher-driven additions and removals ('Novels updated - 3 added')
- Removals wired in FileWatcherService.process() and reconcile() on book-missing events
- findLibraryName() added to ScannerRepository for notification title lookup