Return a user-scoped highlights shelf while preserving the existing single-highlight endpoint. Align calendar dashboard figures with each reader's timezone and completed attempts.
The scroller route returns a DashboardScrollerResponse of books and total, but this spec still
parsed the body as a bare array, so it failed before reaching its assertions.
Allow 0.05% adjustments while preserving existing integer settings and the 98% default. Keep finish detection stable when progress is stored as PostgreSQL real.
* fix(metadata): write finalized metadata back to the imported file
Finalizing a Book Dock file only moved the bytes, so a library with file
writes enabled kept the metadata the file was uploaded with rather than
the metadata the dock settled on. Schedule a write-back for every book a
finalize produces once all of them have their metadata, attributed to
the uploader.
Fixes#1385
* fix(metadata): schedule dock file writes after cleanup
Prevent a failed Book Dock cleanup from leaving a pending file write. Keep the integration assertion synchronized with the debounced writer and restore the missing directory stability mock for end-to-end coverage.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Match subtitle metadata in book searches and index it for large libraries. Add regression coverage for accent matching and library visibility.
+ add open collective
Closes#1552
A file filed into a book that already existed was added without re-ranking, so the
book kept whichever file created it: an ebook that arrived first stayed primary in a
library that ranks audiobooks first. Imports and uploads now re-rank with the
library's priority, keep the current primary on a tie, and a rolled-back finalize
restores the primary it replaced.
Keep one cover slot per medium and show the one each surface needs: the
library's shape for the face, square art on audio screens, the ebook slot
for Kobo and OPDS. Legacy covers convert on upgrade or on first read, and a
write that names no slot keeps working for released iOS builds. Also read
the dev client origin from one constant.
Run the dev API on 6262 and Vite on 6263 with strictPort; production
defaults stay on 3000. Define the dev client origin once and read the
typed appConfig instead of repeating its fallback in every gateway.
Carry positions between sibling EPUBs even without an audiobook, and keep
narration markers on the read-along file only. Point the KOReader book panel
and hold release at the copy devices sync, and label EPUB-only sync in the
read-aloud panel.
Fixes#1508
Track colliding KOReader statistics rows independently and require verified explicit targets before upload. Keep unresolved history pending with a visible recovery path.
Fixes#1457
Recompute the open file digest and recover previously verified matches without guessing between duplicate files. Preserve stranded statistics across lifecycle retries and advertise plugin version 1.5.3 so installed devices can update.
Fixes#1497
The activity overview derived completions from reading sessions that reached 99 percent, while the
dashboard's reading-goal widget counted completed reading attempts. The two disagreed on iOS, where
Home reads the widget and the Annual Goal card reads the overview: a book marked read by hand, one
finished on Kobo or KOReader, an audiobook whose last session stops short of the end, and a re-read
collapsed into its first finish were all invisible to the session heuristic.
Commit 3de365a0 already made completed attempts canonical for the web timeline, goal trajectory and
completion latency. The activity-overview route arrived later with its own session-based query and
reintroduced the divergence on the one surface the web never calls.
Drop the timezone conversion with it. endedOn is a calendar date rather than an instant, so it has
no zone to convert and AT TIME ZONE would only shift buckets across year boundaries.
Fixes#1474
Replace the hardcoded English in the TTS and narration components with
message keys, label every icon-only control, and add settings.common.nav.tts,
which the settings sidebar rendered as a raw key.
Stop requiring PODCAST_ENCRYPTION_KEY while APP_FEATURES.podcasts is off, and
skip podcast routes and permissions in the authorization matrix for the same
reason.
Pass the direct-download settle() helper the SETTLE_MS budget the file already
defines: vi.waitFor's one-second default cannot cover six DNS-resolved redirect
hops on a cold CI resolver.
* feat(settings): add SABnzbd download client
Support API-key authentication, NZB uploads, queue monitoring, reconciliation, and completed-download imports through SABnzbd.
Expose the client in request settings and validate its required category and credentials.
Ref #1312
* feat(settings): add Prowlarr indexer management
Manage synchronized torrent and Usenet sources through a shared Prowlarr connection.
Ref #1312
* fix(settings): correct SABnzbd adoption and managed search retries
Address four defects found in review of the Prowlarr and SABnzbd work.
SABnzbd keeps a failed job in its history until somebody clears it, and BookOrbit removes a client
download only on an explicit action. Because the client key is a stable digest, re-grabbing the same
release adopted that dead history entry, never uploaded the NZB, and failed instantly and forever.
Adopt only an entry that is not already failed; a queue entry still wins, so a fresh upload reports
correctly.
The category-expansion retry reused the title-and-author query, making it strictly narrower on text
than the title-only attempt that had just returned nothing. A release matching only a title-only
search with expanded categories was therefore unreachable. Drop the author there so each rung of the
ladder is genuinely broader than the last.
Changing the download client type through the create form's select bypassed the picker's handler, so
a username typed for qBittorrent was still sent when SABnzbd was chosen. The server stores it, the
API-key form hides it, and it can no longer be edited away. Watch the credential kind instead, so
both controls clear it.
Finally, a swallowed managed-release refresh failure left nothing behind at all; give it its own
fail log while still reporting the original refusal to the caller.
* fix(requests): cast managed search health flags to boolean
The bulk search-health update bound each case branch as an untyped
parameter, so Postgres could not resolve it against the boolean column.
Cast every branch explicitly and assert the cast in the repository test.
* fix(requests): close review gaps in Prowlarr and SABnzbd support
Inventory the eight indexer-manager routes so the authorization matrix
probes them, resolve a managed indexer without reading its manager's
whole source list, keep an unreadable credential out of the test-failed
mapping, translate manager error codes, and show per-source search
health. Documents Prowlarr connections in docs/BOOK_REQUESTS.md.
* i18n(client): keep the retired sources empty-state key
Dropping bodyOneWay from en.json forced pruning it from five Crowdin
target catalogs, which the localization policy check rejects outright.
Leave the now-unused key in place; nothing validates unreferenced
English source keys, so Crowdin can retire it on its own schedule.
* db: regenerate the SABnzbd and Prowlarr migration onto main
Rebasing collided with main's own 0091 and 0092, which left this branch's
two migrations on disk with no journal entry referencing them. Drop both
and regenerate a single migration on main's lineage; the three affected
tables are schema-identical to the pre-rebase snapshot.
Sort groups by reclaimable space, pre-pick a keeper with a rule that states
its reasoning, and expand any row into a field-by-field comparison that shows
only what differs. Persist "not duplicates" so a judgement survives a rescan,
and review every copy before deleting. Reading progress no longer renders 0%.
Add EPUB narration, shared media controls, and native device session integration.
Apply the generated migrations and update all first-party clients alongside the server.
BREAKING CHANGE: introduce incompatible database and API contracts for media playback
Add configurable seed ratio and seed time goals to request indexers, supporting both static goals and tracker-advertised ratio/seed-time limits via torznab attributes. Enforce indexer-specific seed policy during request fulfillment and display active seed targets in the request seed panel.
Closes#1360
Support Usenet indexers via the built-in Newznab adapter and Usenet downloads via NZBGet. Generalize active download tracking from torrent client hashes to protocol-agnostic client keys, add NZB release scoring and file handling, and expose Newznab indexer and NZBGet client options in settings and grab flows.
Closes#1312
Reduce large author listings to one card per series while preserving book counts, scoped visibility, and row-based pagination. Compute both totals in the collapsed query so deep pages remain consistent without a second author-wide scan.
Closes#301
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Derive catalog typeahead values only from books in libraries the requester can access, and enforce their content filters. Preserve superuser visibility, exact-match ranking, and user-scoped collection lookup.
* feat(scanner): set book added_at from earliest on-disk file mtime
A book's "date added" now reflects when its content files first landed on
disk (the earliest mtime across its content-role files) instead of the
moment BookOrbit imported it. Cover/metadata/supplement sidecars are
excluded. When no content file has a usable mtime, addedAt is left unset so
the DB defaultNow() fallback (import time) still applies. Only set at
initial book creation; re-scan/restore paths do not touch it.
* feat(library): add per-library added_at source with recompute
Add an addedAtSource setting to libraries so "date added" can follow the
book's files on disk rather than the moment the scanner happened to see
them. The setting takes imported (unchanged default), file_modified, or
file_created, guarded by a check constraint on the column.
Expose a recompute action so the chosen source can be applied to books
that are already imported. file_modified runs as a single set-based
update over the stored mtimes; file_created has to stat the files, since
birthtime is not persisted, so it batches those reads.
Leave every existing library on imported so behavior is unchanged unless
a user opts in.
* feat(library): add added_at source control to the library editor
Surface the new addedAtSource setting in the library editor as a radio
group, alongside a button that triggers the recompute for the library
being edited.
Keep the recompute out of the save path so changing the setting stays
cheap: the setting alone only affects books imported from then on, and
applying it to existing books is an explicit choice.
* i18n(library): translate added_at source strings (de/nl/pt/sl)
* fix(library): correct create payload, recompute floor and failure logging
The library creator posts its whole form, so omitting addedAtSource from
CreateLibraryDto made the global whitelist reject every create request
with a 400. Declare the field, validate it against the same source list
as the update DTO, and persist it so the initial scan uses the requested
source rather than silently falling back to imported.
Apply the scanner's usable-time floor to the mtime recompute query too.
It previously excluded only NULL, so a filesystem reporting epoch 0 would
rewrite added_at to 1970 instead of leaving the import time in place. The
floor now lives in library.constants rather than being spelled out twice.
Wrap the recompute in try/catch so a database or stat failure emits the
[fail] log the logging conventions require instead of propagating with
only a [start] line recorded.
* fix(library): gate recompute on the saved source and label the radios
The recompute button stayed enabled while the selected source differed
from the stored one. Because the request carries no source, clicking it
recomputed against the old stored value while the UI showed the new
selection. A hint already said to save first; disable the control as well
so the mismatch is not actionable.
Give the source list radiogroup semantics with aria-checked so screen
readers can tell which option is active, since selection was conveyed by
styling alone.
* test(library): assert the create DTO under the real validation options
The previous comment claimed the case exercised whitelist and
forbidNonWhitelisted, but the shared helper calls validate without them,
so it only proved @IsIn rejects an unknown value.
Drop the misleading comment and add a case that validates with the same
options main.ts gives ValidationPipe, so an undeclared addedAtSource
fails the way it would in a real request.
* docs(library): remove redundant date-added documentation
---------
Co-authored-by: Bob Bobber <user@example.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Recurse into nested subfolders during book dock watcher scans and discovery so that books organized under category or author subdirectories are processed. Keep shared folders from claiming exclusive directory ownership so siblings and multi-track units can finalize and discard cleanly without collisions.
Closes#1311
Preserve the stored v1 identity while resolving primary hashes through bounded,
cached lookups for polling, retries, tracker checks, removal, and reconciliation.
Cover alias handling and lookup failures with unit tests and verify completion,
import scheduling, duplicate adoption, and removal against qBittorrent 5.2.3.
Closes#1318
Require an enabled OIDC-linked superuser before disabling password flows. Track session authentication methods and fail closed across the API, web client, WebSockets, and administrative mutations.
Closes#1002
* feat(reader): add PDF highlights and annotations
Persist and render PDF highlights through the same annotation system as
EPUB. The annotation_positions table already supported a 'pdf' format, so
this wires the web create path, response, and reader UI to it.
Backend:
- create-annotation DTO accepts a pdf position (page + segment rects) as an
alternative to a cfi; exactly one is required
- repository createPdf() persists a format='pdf' position (geometry in pos0,
pageno in extras); selectWithCfi exposes the pdf geometry and status
- response DTO parses the pdf position and derives positionStatus
Frontend (pdf-v4 reader):
- usePdfAnnotations REST store and usePdfHighlights orchestration render
persisted highlights via the EmbedPDF annotation plugin (autoCommit off,
DB stays source of truth)
- selection popup (colour/style/note/copy/delete), note dialog, and a
Highlights sidebar tab with search/colour/notes filters, navigate, delete
- NoteDialog relocated to reader/shared for reuse by both readers
Shared types gain AnnotationRect/AnnotationPdfPosition and an optional pdf
field on AnnotationItem.
* fix(reader): preserve input on failed PDF annotation save; convert mixed PDF+CFI for KOReader
- usePdfHighlights: createFromSelection/restyleExisting now return success;
applyHighlight and saveNote only clear the selection, dismiss the popup, and
close the note dialog on success. On failure the mark, popup, and typed note
are preserved so the user can retry without losing input.
- koreader annotation exchange: the PDF push-down guard now skips only PDF-only
annotations (no EPUB anchor). A mixed PDF+CFI annotation still converts its CFI
to xpointer instead of being dropped.
- Tests for both paths.
* fix(reader): label PDF selection buttons and dedupe multi-page highlight retries
- PdfSelectionPopup: add aria-labels to the copy, highlight, note, and delete
icon-only buttons so screen readers can identify them.
- usePdfHighlights: createFromSelection keeps only the pages that failed, so a
retry after a partial multi-page failure never duplicates already-saved pages.
- Test for the multi-page partial-failure retry path.
* fix(reader): harden PDF annotation store, validation, and create logging
- usePdfAnnotations: catch rejected api() calls (session expiry / network) so
load() sets loadError and mutations return null/false instead of rejecting.
- create-annotation DTO: require positive rect width/height so degenerate PDF
geometry is rejected; tests for zero/negative dimensions.
- annotation.service: add [annotation.create] start/end/fail lifecycle logs with
IDs, durationMs, and sanitized error for the multi-step create.
* test(reader): cover PDF annotations in the hub and add missing docstrings
Adds a real-DB e2e proving a PDF highlight persists as a pdf position and surfaces in the library-wide Annotations hub with the exact fields the page deep link needs (jumpFileFormat=pdf, cfi=null, pageno). Adds a hub-service unit test for the pdf row mapping. Documents the PDF render helpers, the annotation deep-link builder, and the createPdf repository method.
* fix(annotations): make PDF highlights reliable across reloads
Reconcile persisted highlights after EmbedPDF finishes loading its annotation state. Keep file-scoped pagination bounded and prevent PDF-only positions from starving device sync.
Ref #1195
* fix(annotations): keep pdf annotations stable during reload
Use authenticated native pagination fetches and discard responses that overlap successful mutations. Align shared dialog styling and annotation tests with project conventions.
Ref #1195
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Keep KOSync limited to position synchronization and rely on measured page timings from the BookOrbit plugin for KOReader reading sessions. Remove legacy estimates at startup and rebuild affected daily totals.
Fixes#1233
Let users request books, route approvals, search configured sources, and import fulfilled releases through the Book Dock. Include scoped permissions, automation, download-client integrations, plugin support, requester visibility, and one consolidated schema migration.
Closes#248
Ref #1115
Preserve target-only statuses and update conflicts only when the source carries newer evidence. Treat timestamp-free source rows as insert-only so they cannot overwrite curated target state.
Fixes#1104
Expose promotion and demotion to existing superusers while keeping shared and self targets blocked. Serialize role lifecycle changes, preserve one active superuser, and invalidate stale credentials.
Closes#1208
Treat partial-hash matches across distinct books as unresolved so progress cannot be written to an unrelated file. Preserve same-book duplicates and user-scoped manual links as valid disambiguation paths.
Fixes#1135
Normalize legacy float values before migration 0076 so 2.7 databases can upgrade without constraint failures or precision loss. Serialize the compatibility conversion across concurrent startup processes.
Fixes#1189
Add an isPublic flag to collections allowing users to share collections across the instance. Public collections are readable by all authenticated users while write, edit, reorder, and delete operations remain restricted to the owner.
Add fileSize as a filterable rule field resolved against the book's primary file size in bytes. In the filter builder UI, allow operators to input sizes in megabytes with automatic serialization to bytes for queries.
The series, author and similar endpoints never selected a read status, so
every cover in the detail shelf rendered bare. Attach the requesting user's
status in one bulk lookup per lane, and draw the badge in BookCarousel using
the same icons and placement as the library grid, plus an sr-only label.
BookRepository has no user in scope, so it now returns UnscopedBookRecommendation
and the type system forces the status on where the user exists.
Also adds the untracked scanner overlap repro for issue 1094.
Closes#1163
Any write to book_metadata bumped updated_at and minted a fresh
CoverImageId, so a device holding that cover locally fell back to the
placeholder offline. Stamp cover_updated_at at each cover write instead;
migration 0080 backfills it from updated_at so no id churns on upgrade.
Also trims the DECIMAL(10,2) scale Booklore hands back for a series
index, which sorted 2.50 after 2.9. Closes#943.
Decode added dates and manual reading dates with an explicit parser instead
of new Date(), which reads a zero-padded year below 100 as a two-digit year:
years 0013 to 0031 became an Invalid Date and made every listing holding the
book answer 500, and years 0001 to 0099 shifted silently into 19xx or 20xx.
The column type is unchanged on the wire, so no migration is needed.
Closes#1143
Imports resolved the naming pattern with the download resolver, which keeps only the last
path segment, so the shipped default's author and series folders were dropped and every file
landed at the library root. Rename, bulk rename and move all kept them, leaving the app
disagreeing with itself about where its own import belonged.
Closes#1157
Finishing an issue offered no way onward, so the next one had to be opened from the library.
Resolve the next readable book server-side with one keyset query, offer it on the last page, and
let an opt-in setting cross over on a further page turn. The reader route now remounts on a param
change, without which it would keep rendering the book it was mounted with.
Also cast the compared value in compareSeriesIndexSql: as a bare parameter Postgres could not type
it inside the null test and rejected the statement.
Closes#998
Use completed reading attempts as the canonical source for timelines, goals, and completion latency. Preserve user and library scoping while excluding deleted, skimmed, and undated attempts.
Fixes#1132
Treat series positions as validated string labels so values such as 5.10 retain their exact representation across storage, metadata providers, file formats, and clients.
Closes#1119
Rebuilds the users roster, metadata field rules, file naming and metadata
score panels, and replaces the libraries list with an expandable ledger.
Libraries now batch their counts and last scan into one request instead of a
per-library fan-out, and surface scan history from scan_jobs on demand.
Editing Date Started or Date Finished sent a date-only patch that the attempt layer had nothing to
attach it to. On an unread book no attempt was created, so the request returned 200 with null dates
and the field cleared itself. On a book being read the date landed on the open attempt as an end
date with no outcome, violating reading_attempts_closed_has_outcome_chk and surfacing as a 500.
Treat a date-only patch as a lifecycle statement: a start date opens an attempt and projects
reading, a finish date closes one as completed and projects read, and endedOn is never written
without its outcome. Clearing a finish date keeps the completion, because an outcome with no
completion date is legal and is already what re-read seeding writes. Setting a finish date on an
abandoned book now completes it. Reject a status that contradicts its dates with a stable error
code the client can translate, and derive statusChanged from the resulting status so a date-driven
transition still reaches achievements and the Kobo projection.
Keep unread and want_to_read free of dates they cannot own. The attempt keeps its own, so a book
marked unread no longer projects a start and finish date once a historical attempt is logged, and
marking a finished book unread no longer strands its finish date on the projection.
Stop the Hardcover import sending dates a status cannot carry. A want-to-read row that still holds
a start date would otherwise fail its row and skip the status import entirely.
Closes#1073
Read selection was positional: prefer any open remote read, else a cached book-level id, else the
first read returned, while the primary attempt was re-derived each sync from book dates. The two
could disagree, assigning one Hardcover read to two attempts and violating
reading_attempts_external_uidx, which aborted the sync and persisted the failure.
Treat reading_attempts.external_id as the mapping of record: an attempt keeps the read it owns, an
unlinked attempt adopts only an unclaimed read and claims it locally before the remote write, and a
fresh read is minted when nothing is adoptable. Never merge or delete attempts to resolve a
conflict, and keep soft-deleted ids reserved so importExternalRead tombstones still hold.
Also skip the link write when an attempt already owns its read, and map the aggregated attempt
timestamp to a Date so change detection stops comparing a driver string against lastSyncedAt.
Closes#907