Return saved organization and automation settings in the user-scoped library list so
non-superuser summaries reflect the saved configuration.
Cover enabled and disabled settings, library isolation, and permission denials with
API regression tests.
Closes#1664
Keep dashboard layouts browser-local by default and let users opt in to account storage.
Group library selection and shelf sync in one dropdown, preserve legacy client settings,
and block sync changes for demo-restricted accounts.
Fixes#1646
The collapsed order by had no metadataScore case, so the sort was dropped
and the list fell back to title order. Order by metadata_score there and
select it in the collapsed jump-bucket query that shares the order by.
Fixes#1638
Use the package media:duration when a SMIL clip cannot be measured.
Storyteller 2.x writes a backwards clip at audio track boundaries, which
nulled the total and blocked the audiobook bridge. Stored nulls are
re-measured via a migration, the detail page and an unchanged-folder scan.
Fixes#1619
The three-column layout left the editor 270px wide at 1280 and broke resolved paths mid-word.
Draw the pattern one folder per line, preview it against long-name books, and list only what
missing metadata changes. Add resolvePlaceholders to the shared resolver to report fallbacks.
Keep TypeScript at 6.0.3 while moving the remaining server and client packages onto their current supported releases.
Allow the PWA asset generator 2 peer range because vite-plugin-pwa still declares the compatible version 1 range.
* fix(authors): keep non-latin letters when matching enrichment candidates
Keep letters and digits of any script in normalizeName() instead of
dropping everything outside [a-z0-9], and stop treating two empty token
lists as a token-set match. Names without ASCII letters normalized to an
empty string, so any two of them scored 0.96 and passed the confidence
gate, and enrichment linked authors to unrelated people.
Closes#1459
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(authors): keep combining marks when normalizing author names
Keep \p{M} in normalizeName() alongside letters and digits. Vowel signs
in scripts such as Devanagari are combining marks, not letters, so
dropping them merged distinct names: कमल and कमला both normalized to
कमल and matched with score 1. Latin diacritics are still folded by the
earlier U+0300-U+036F strip, so Latin and Cyrillic matching is unchanged.
Refs #1459
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Bump every direct dependency to its newest same-major release, moving the
embedpdf patches and packageExtensions to 2.15.1 and deduping the lockfile.
Hold jsdom at 30.0 because 30.1 breaks vitest's URL.createObjectURL shim,
and skip releases still inside pnpm's minimum release age.
Expose media-overlay-aware naming across file creation and delivery paths while preserving existing client contracts. Keep settings router mocks compatible with the application router exports.
Closes#1590
Handle both idle pool errors and checked-out client errors so a database restart fails only the affected operation. Keep connection-loss logging sanitized and deduplicated while allowing the pool to reconnect.
Fixes#1581
Remove device-scoped annotation delivery state with the rest of its KOReader data so stale acknowledgements cannot block permanent deletion.
Fixes#1576
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.
Clients that download an audiobook get its files from the book detail and its playback order from
the manifest, and had to pair the two by position. The two lists are ordered differently, so an
index pairing can attach the wrong file to an asset. Add the book_files id to each manifest asset
so the pairing can be made by id.
The field is additive and the server always sends it. The manifest revision is hashed from file
and chapter data, not from the response, so existing revisions do not change and clients holding
one do not see a 412 from this deploy.
The iOS app consumes this field to pair downloaded files with manifest assets; the web client
does not read it yet, so a grep of the client alone will show it as unused.
A multi-track audiobook without stored chapters gets one synthesized chapter per track, placed at
the running total of track durations. An unknown duration counted as zero, so every later
chapter started at the wrong offset, often all at 0, and the manifest then mapped them all onto
the last asset. The synthesis also walked the tracks in file name order while the manifest plays
them by sort order first, so chapter titles could disagree with the track they pointed at.
Skip the synthesis when any track duration is unknown, as already happens for a single track;
clients fall back to one entry per track. Order the synthesized chapters with the same comparator
the manifest uses, now shared from book-media utils. The detail files list keeps its existing
order. Affected books get a new manifest revision because the revision hashes the chapters.
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.
The extractor stores durations as whole seconds, so a manifest asset can report a duration up
to half a second shorter than the audio. The playback-state PUT and the audiobook bookmark
create refused any position past that rounded value with a 400, which rejected the tail of every
such track and left clients retrying a write that could never land.
Allow one second of slack at both checks and clamp the stored position to the duration. This
only relaxes validation: every position accepted before is still accepted and stored unchanged.
Audio bookmark deletes are soft, but the unique index on (user, book, position_seconds) still
covered tombstones, so the insert conflicted and the audiobook bookmark route answered 409 for
any position that had ever been bookmarked and deleted. Limit the index to live rows. The CFI
index is left as it is because KOReader bookmark revival relies on tombstones keeping their CFI.
The new predicate only narrows the index, so existing rows cannot violate it.
Let users hide request entry points and notifications while existing requests continue running. Keep the setting optional so older clients retain their current behavior.
Ref #1476
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): keep a pinned hardcover edition past the edition cap
resolveCachedMatch looked the cached edition up in the capped 50-edition list of the work, so on
works with more editions (Harry Potter, Le Petit Prince, Dune) a manually chosen edition past the
cap was never found and pickBestEdition silently re-pointed the book on the very sync that
setEdition triggers. Resolve the cached edition by id, scoped to the book, and only fall back to
the capped list when Hardcover no longer has that edition on the book.
Fixes#1252
* fix(metadata): serialize Hardcover edition changes with sync
Keep a manual edition choice from being overwritten by an older in-flight sync. Queue sync and edition operations per user and book so the manual choice runs after earlier work.
Ref #1252
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
* 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.
Every format chip, edition row and format menu now builds from shared helpers: content files
only, a read-along EPUB kept apart from a plain one, in the library's format priority, which
gains a read-along entry. Labels, colours and the chip each have one source, and cards, OPDS,
email, Kobo and the dashboard rank files the same way. Book detail file order is unchanged
because iOS builds its track queue from it.
Evaluate identified KOReader clients independently so a stale sibling device does not suppress safe updates. Keep the conservative account-wide gate for legacy or unusable client identities.
Fixes#1519
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
Accept android as a reading-session source from clients and report it as
its own statistics bucket, labelled Android app, beside the iOS app and
Apple Watch. A generated migration adds it to the reading_sessions check
constraint, achievements count Android reading toward Full Orbit and Two
Worlds, and the web charts draw it in a new --pill-android token at hue 95.
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
Request cached tags for ISBN and stored-ID GraphQL lookups. Normalize the Genre bucket so identified books retain the same genre metadata as free-text search results.
Fixes#1422
Block text was unescaped from a six-entry table, so only &, <, >, ",
' and ever became characters. Every numeric reference survived into the
text handed to the TTS provider, and an EPUB that writes its apostrophes as ' had
them read out as "hash thirty-nine" while the page showed a normal apostrophe. Which
apostrophes broke depended only on how the book was serialized: ' was in the
table, ' was not, and both appear in the same sentence often enough.
Parse each block instead of stripping tags and unescaping by hand. The parser decodes
whatever the reader's own DOM decodes, which is the property that matters here because
the narration is read against the page, and it decodes in one pass, so a book that
quotes &#39; is still narrated as '.
Only the text conversion changed. Block boundaries still come from the same split on
block-element opening tags, so block indices stay aligned with Foliate's highlight
blocks and audio cannot drift a paragraph away from the highlight.
Do not route this through htmlToPlainText. That helper strips tag-shaped text a second
time after decoding, which is right for the scraped metadata it was written for but
silences book content: a code sample written <div>hi</div> would narrate as
"hi", and "5 < 6" would lose its operator. Tests cover both.
Fixes#1486
A cover refresh skipped thumbnail generation whenever the stored cover
source was custom, so re-extract reported success while /thumbnail kept
404ing. Generate the missing thumbnail from the custom cover, and repair
any other gap lazily on the first thumbnail request.
Fixes#1475
PUT reader/defaults/:formatGroup demands the full settings object, so a client has to send every
field of a group to change one of them. That forces a client to own fields it does not model and to
echo back values it never shows, and it turns two clients editing different fields into a
last-writer-wins race over the whole row.
Add PATCH for both defaults and per-book preferences. The defaults patch merges over the stored
row with the built-in defaults folded in underneath, so the row stays a complete object for the web
client's sanitiser while a partial caller leaves no holes. The per-book patch takes set and unset
and resolves both in one statement, so a concurrent patch cannot lose a field to a read-modify-write,
and deletes the row once its last key goes, which is the same state as never having been customized.
Nulls are never stripped: a null fontFamily means "use the book's own font", not an absent key.
Keep only the keys the caller actually sent. Zod applies .default() while parsing a partial, so a
cbx patch that never mentioned spreadGap came back carrying one; the existing per-book PUT had the
same latent bug and now shares the fix, which stops a touched comic from being pinned to the
default gap.
iOS is the only caller of the new routes. It keeps the layout fields on the device and syncs the
look fields, which is exactly the split a partial write makes expressible. The web client does not
call them and still PUTs full snapshots.
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.
Reading attempt date keys came from toISOString(), which is UTC's day, so an evening finish west
of Greenwich filed tomorrow while the reading session it came from filed today. Callers now pass
the instant and UserBookStatusService resolves the reader timezone itself, because device syncs
carry no request user and a caller that forgot would silently fall back to UTC.
Closes#1458
qBittorrent 5.2 renamed its session cookie to QBT_SID_<WebUI port> and removed the setting that
overrode the name, so reading only SID left the session empty and every call answered 403. Match
both names, and report a 401 login as rejected credentials now that 5.2 answers a bad password
that way instead of with "Fails.".
Fixes#1306
* 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%.
PODCAST_ENCRYPTION_KEY is required in production but was absent from the
entrypoint file_env list, leaving it as the only secret with no _FILE path.
Test every supported name rather than spot-checking a single one.
Move secrets-file and OIDC hardening detail to bookorbit.app/installation.
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