978 Commits

Author SHA1 Message Date
neon 9a7970f0b3 feat(metadata): list every visible language for a blank language query
The iOS library filter offers languages as chips and needs the whole list, not a typeahead. A blank
term on /metadata/languages now returns every language value on a visible book, most common first
and capped at 200, under the same library and content-filter scope as the search. Every other
suggestion endpoint still answers a blank term with nothing.

The web client never sends a blank term. Released iOS builds do, when the Smart Shelf language
picker opens, and now see the list there instead of a search prompt. The iOS filter calls this
route too, though nothing in the web client shows it.

Ref bookorbit/bookorbit-ios-support#54
2026-10-12 05:30:58 +00:00
neon 30229c7451 fix(reader): open EPUBs whose package lost its default namespace
epubBuilder writes the OPF root as `mlns=` for `xmlns=`. Once a metadata save
bound `opf:` on <package>, foliate matched OPF elements by namespace, found no
spine, and failed with "reading 'children'". Match by namespace only when the
root is in it, and have the writer add the missing default namespace so
rewritten files also open in released iOS builds.

Closes #1680
2026-10-12 05:09:00 +00:00
neon 688cfdeaec fix(scanner): keep audiobook tracks in file name order
The player follows each track's stored sort order, which overlapping
folder scans, Add File, and imports joining a book could leave out of
step with the file names the Files tab shows. Realign a book's tracks
after each of those writes, under the book row lock.

Closes #1681
2026-10-12 05:05:13 +00:00
neon 4d453c82ca fix(books): measure audiobook durations when Book Dock files them
Book Dock finalize, which also files book requests, left each audio file's
duration empty until the next library scan. The player derives progress only
from those, so a filed audiobook stayed at 0% and never reached Continue
Listening. Finalize now measures them once metadata is applied.

Refs #1757
2026-10-12 03:44:40 +00:00
neon 4440fea99a fix(server): keep tracker timeouts recoverable
Keep downloads active after transient tracker failures so later completions
reach the importer. Reserve early failure for explicit rejections and import
completed content even when the tracker still reports an old rejection.

Cover timeout recovery, transient failures, and explicit rejections with unit
tests.

Closes #1607
2026-10-11 07:02:19 +00:00
neon 4770032ce7 fix(metadata): import ComicInfo Tags as book tags
Map <Genre> to genres and <Tags> to tags instead of using tags as a genre
fallback, which dropped them whenever a Genre was present. Read the
ComicBookInfo genre field so those comics keep their genres.

Fixes #1572
2026-10-11 05:58:02 +00:00
neon 7e11d39724 fix(opds): send book descriptions as plain text
Descriptions are stored as HTML but the feed sent them escaped under
type="text", so clients that honour the type, such as Foliate, showed the
tags literally. Flatten them with htmlToPlainText, keeping line breaks.

Fixes #1569
2026-10-11 05:58:02 +00:00
neon c37821782a security(deps): update dependencies and rebuild kepubify with go 1.27.2
Bump every direct dependency to its newest same-major release and move the
node base image to 26.11.1, which invalidates the cached apk layer and picks
up tiff 4.7.2 for CVE-2026-4775. Rebuild all six kepubify binaries from the
pinned upstream commit with Go 1.27.2 for CVE-2026-78667, CVE-2026-78669 and
CVE-2026-97031, keeping v4-dev so rendition hashes stay stable.
2026-10-11 05:19:04 +00:00
neon e4843e4aa1 fix(metadata): keep extended m4b tags when a cover is written
Write the tags FFmpeg's iTunes mode drops (subtitle, publisher, language,
series, ASIN) as freeform atoms, and carry over the source file's own
freeform tags such as Audible's. Covered M4B and M4A writes lost them.

Ref #1744
2026-10-11 04:37:26 +00:00
neon cc50b55eeb feat(books): add read-aloud delivery policies and preserve sync
Add original and text-only EPUB delivery choices across Kobo, KOReader,
OPDS, web downloads and email while keeping source files unchanged.
Persist server defaults, device overrides and captured artifact identities
so retries and guided replacement retain the selected rendition.

Preserve saved progress across first opens, replacement resets and stale
updates, and project annotations against the copy delivered to each device.
Handle XML resource references, large inodes, retry backoff and background
worker state correctly. Consolidate the schema changes into one migration
and retain released-client request and response compatibility.

Verify with the full server and client checks, delivery and sync HTTP suites,
independent code reviews, and live KOReader download/settings/reader proof.

Fixes #1496
Closes #1603
Closes #1714
Fixes #1723
Ref #1633
Ref #1508
Ref #1621
Ref #1136
2026-10-11 03:11:27 +00:00
neon 7bb51a7a0b feat(metadata): cache metadata provider answers
Repeated searches and refreshes asked every provider again, which is slow and runs into provider
rate limits. Keep each provider's answer in Postgres, shared by all users, and reuse it until an
admin-set lifetime runs out. Search screens mark saved results and offer a fresh search, and
explicit refreshes stay live, falling back to a saved answer only when a provider fails.

Empty answers are only kept for API providers that report failures reliably, and a cache-only
request never calls a provider in cooldown. Book Dock's manual refetch stays live-only because
the dock has nowhere to report a fallback. Released iOS builds keep working: every new request
field is optional and newer clients gate it on the metadata-search-cache server feature.
2026-10-10 22:26:26 +00:00
neon c91648eaac feat(books): include comics in non-audio requests
Keep books and comic archives under the existing ebook request kind so searches, ownership checks,
format preferences and release profiles agree without changing the API contract. Cover the combined
formats and library access rules with unit and end-to-end tests.

Ref #1748
2026-10-10 21:47:09 +00:00
neon e6c0275fdf fix(settings): refresh upload limits and default to 4 GB
Share the saved upload limit with validation so changes apply without a page reload.
Seed new installations with a 4096 MB limit while preserving existing values.

Fixes #1733
2026-10-10 17:30:22 +00:00
neon 5a4560c9e2 fix(metadata): make epub metadata writes idempotent and lossless
Each save left the previous save's entries behind when a package bound the OPF namespace to a
prefix other than opf, added another title refinement on EPUB 3, and kept Dublin Core written
without the dc prefix beside its replacement. Match elements regardless of prefix and remove the
refinements of whatever is replaced.

Stop both OPF parsers reading untouched text as numbers and re-escaping character references,
which dropped leading zeros from identifiers the writer was meant to leave alone.

Fixes #1706
2026-10-10 16:30:24 +00:00
neon af8f55976f feat(client): add the Nostalgia appearance style
Add a Style setting beside mode, accent, radius and background. Nostalgia restyles the app through
tokens only (neutral surface palettes, system type, flat shell, top glow) and pins background,
opacity and brightness. Add a top glow reach slider for both styles. Tidy the theme layer first:
charts read live tokens, bare `rounded` follows the radius scale, one shared sign-in toolbar.
Also carries the settings page header redesign.
2026-10-09 01:01:57 +00:00
neon c63f3ab757 fix(metadata): choose sources by medium and stop overwriting book data
Lead fetches with the primary file, give Book Dock its library's rules and read-along media,
default audiobook fields to sources that carry them, and flag providers that never fill a field.
Joining a format now fills gaps only and respects locks; a move picks its primary as a scan would.
2026-10-08 18:12:48 +00:00
neon 39ac2f1efd feat: request an ebook and audiobook as a pair
Link two ordinary requests as companions, filed into one book, with a Both search,
grouped list rows, a tabbed release picker and a book-page request for the missing
format. Retire comic requests, keeping old rows readable. File Book Dock covers by the
field rules with a shape check, and re-pick a moved book's primary by its new library.
2026-10-08 16:12:30 +00:00
neon 01b5638ea7 feat(cover): show each book's cover in the shape of its primary format
Pick the face slot from the primary file's medium instead of the library's
ratio, and send that frame per book on cards, detail, notebook labels and
recommendations. Grids keep uniform tiles, and the library setting becomes
Grid shape.
2026-10-08 06:19:00 +00:00
neon c35eb38485 fix(cover): restore audiobookcovers searches
Use separate title and author parameters to match the current upstream search form and restore cover results. Add regression coverage for title-only searches, whitespace trimming, and query encoding.
2026-10-07 21:33:50 +00:00
neon 0d426db88e fix(scanner): preserve separate books at the library root
Use file identities for root-level imports and renames to match scanner
grouping and prevent unrelated books from sharing metadata.
Cover repeated scans, metadata write-back, and stable reading and file
identities with regression tests.

Fixes #1599
2026-10-07 20:51:27 +00:00
neon 5c093333c5 fix(smart-scope): refresh sidebar counts after saving
Return matching item counts with create and update responses so the sidebar
can display the saved scope count without requiring a page reload.
Reuse the listing count logic and verify visibility and shared sidebar state.
2026-10-07 20:12:54 +00:00
neon d719c4ee5b fix(metadata): preserve ISBN fields across metadata workflows
Carry ISBN-10 and ISBN-13 independently from accepted providers through
Auto-fill previews, saves and background refreshes. Preserve existing
identifiers and field locks, validate provider checksums and avoid
combining different editions.

Classify embedded ISBNs correctly and support ISBN-10 fallback in
search, Book Dock confidence and file naming.
2026-10-07 20:07:23 +00:00
neon 3ef4a22c37 fix(scanner): play disc-folder audiobook tracks disc by disc
The walker sorted a book's files by file name, so tracks in disc folders that reuse names, such as
CD 1/01.mp3 and CD 2/01.mp3, interleaved in playback, merged chapters, and written track numbers.
Order them by their whole path instead, everywhere a track order is chosen.

A rescan that only reorders a book's existing tracks now moves every audio bookmark and the
listening progress percentage to the same moment of the same track, and rebuilds merged chapters.
Disc folders are never skipped by incremental scans, so existing libraries are fixed by their next
scan.

Also inventory the notebook and dashboard highlights routes in the authorization matrix, and pin the
app locale in two date tests that depended on the machine's locale.
2026-10-07 18:31:33 +00:00
neon 100953ea1b feat(metadata): write metadata to every file of a multi-file book
Books often carry several files, such as an EPUB beside a PDF or an audiobook split into tracks.
Write-back reached only the primary file and the audio tracks, so the other formats kept stale
metadata.

Add an opt-in per-library mode that writes every content file of a book. Target selection,
capability reporting and the Files tab now share one role-aware selector, covers resolve per
medium, and results report per-format counts. Read-along EPUBs get their own toggle, off by
default, and their own size limit of 1000 MB, so they no longer follow the EPUB settings.

Live testing on copies of real libraries surfaced write-path bugs, fixed here as well:
- tag-only writes to chaptered M4B files failed because FFmpeg copied the chapter data track
- an undecodable cover failed every file instead of falling back to metadata only
- failure reasons stored the FFmpeg command line and absolute paths; they are now sanitized
- size limits used the stored size, so an unknown size bypassed them; writes now check the disk
- atomic replace dropped the file mode and ownership
- the EPUB temp name failed on long file names and could clash with a user file
- tracks in disc folders got duplicate titles
- a PDF worker that exited without a result hung its write forever; it now fails and times out
- null on a NOT NULL library setting returned 500 instead of 400

Generates migrations 0104 and 0105. Every new API field is optional, and released iOS builds
decode none of them.

Ref #694
2026-10-07 16:52:52 +00:00
neon e6ea5bd77e fix(stats): invalidate reading caches after committed changes
Refresh dashboard and statistics caches immediately after reading history,
projection, reset, import, and shared deletion changes. Coalesce bulk
notifications while keeping committed changes visible to concurrent reads,
and isolate listener failures from successful writes.

Skip unchanged Hardcover attempt updates and protect cache generations
against stale in-flight loads. Cover date edits, projection-only resumes,
rollback, bulk failures, user scope, and cache races with regression tests.

Ref #1695
2026-10-07 01:28:10 +00:00
neon 58756565fc fix(kobo): preserve koreader matching for converted downloads
Register the identity of delivered bytes so Kobo conversions and audio
removal continue matching their source books in KOReader. Keep download
registration recoverable without allowing database lock waits to stall it.

Preserve manual corrections across cached device identities, make review
semantics explicit, and keep cache invalidation bounded for large libraries.
Add unit, database, lifecycle and scale regressions with one generated migration.

Closes #1697
2026-10-07 00:07:50 +00:00
neon 019c432262 fix(opds): expose public collections to other users
Align collection visibility across OPDS and KOReader with the web catalog.
Count only books the viewer can access in one aggregate query, and cover
public access, private revocation, and filtered counts with regression tests.

Closes #1701
2026-10-06 22:16:21 +00:00
neon 1abf2f58ca fix(kobo): sync native kepub progress and canonical positions
Allow native KEPUB files to exchange precise reading progress, bookmarks
and highlights with Kobo while preserving sync permissions and settings.

Use the original KEPUB archive for Kobo spans and project its added
wrappers away for shared EPUB CFI and KOReader XPointer positions.
Preserve existing EPUB conversion behavior and API compatibility with
released iOS clients.

Cover native KEPUB context, canonical positions and two-way exchange with
unit tests and paired EPUB/KEPUB API E2E scenarios.

Ref bookorbit/bookorbit-ios-support#36
2026-10-06 20:55:45 +00:00
neon 2dddf9b89f fix(books): retain records when content deletion fails
Remove content before committing book deletion so surviving files cannot be reimported
under new records. Preserve records and covers on failure, bound cleanup concurrency,
and allow retries after partial removal. Defer watcher events through the database
commit and cover delayed operations, failure recovery, and retry behavior in tests.

Ref bookorbit/bookorbit-ios-support#42
2026-10-06 20:54:22 +00:00
neon 4ca7128b76 fix(library): refuse renames into a folder another book holds
In folder-as-book libraries a pattern that files several books under one folder either failed on
the folder_path unique index or silently merged a different book into the folder's owner, deleting
its reading history. Check the folder owner on every rename path and merge only when both books
share a title and primary author; otherwise skip with a clear reason. The bulk preview now reports
these as collisions.

Fixes #1682
2026-10-06 20:54:22 +00:00
neon a24dc2e2cc feat(annotations): add a library-wide notebook for every book
Add read-only /notebook routes that list highlights, journal entries, bookmarks and reviews
across every book a reader can open, as one cursor-paged stream with per-kind counts, a By Book
listing, a daily review deck, a seeded review shuffle, On this day and a library-wide trash. Each
kind is read through its own index-ordered arm cut at the page size before a UNION ALL merge, so a
deep page costs what the first does, and book labels travel once per page in a sidecar map.

Scope follows library access and content filters. Everything is additive and advertised as the
notebook-hub app-info feature. The iOS app calls these routes and the web client does not yet, so
they look unused from the client alone. No migration: the extra indexes for the edited sort and
search wait until the chapter-editor migrations land.
2026-10-06 20:54:22 +00:00
neon ff880a470c feat(annotations): add book journal, starred highlights and colour names
Add per-book journal entries keyed by a client id, a star toggle that does not bump the annotation
version pushed to devices, a per-user colour legend, and an edit route so bookmark title and note
changes reach the server. All of it is additive and advertised through app-info features, so iOS
sends new fields only to servers that support them. Also sort annotations by numeric CFI steps
instead of comparing CFI strings.
2026-10-06 20:54:22 +00:00
neon 74d4a669df feat(reading-session): return session type in the per-book session list
Include whether each session was spent reading, listening to text to speech or listening to audio,
so clients can tell Read Along listening on an EPUB apart from reading it. The column was already
stored; only the per-book list left it out. Unknown stored values read as reading.

The field is optional and additive: released iOS builds ignore it, and the iOS reading timeline
treats it as optional and falls back to the file format on servers without it. The web client does
not read it yet.
2026-10-06 20:54:22 +00:00
neon e253f7ffca fix(reader): preserve exact positions when syncing with koreader (#1694)
Convert complete source CFIs on the server to avoid chapter-start resets
and browser-generated locators that disagree with CREngine. Preserve
engine whitespace, Unicode offsets, element anchors and long text nodes.
Handle prefixed OPF namespaces and retry outdated annotation conversions.

Closes #1621
Closes #1288
2026-10-06 11:22:55 -06:00
neon df926fdf0f fix(library): include saved settings in scoped library summaries
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
2026-10-03 16:45:15 +00:00
neon cb8d1e3b2b fix(settings): add optional dashboard shelf sync
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
2026-10-02 04:11:47 +00:00
neon f8c8ecf99a fix(books): honour the metadata score sort when series are collapsed
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
2026-10-02 00:10:28 +00:00
neon 068a62b7ab fix(reader): fall back to the declared narration length for read-along EPUBs
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
2026-10-01 18:09:27 +00:00
neon 4701412ef2 feat(settings): redesign file naming as a two-column workbench
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.
2026-09-30 22:12:02 -06:00
neon 05a55d6b62 build(deps): upgrade non-TypeScript dependencies
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.
2026-09-30 21:37:55 +00:00
SergeiAK d1b3fae7f7 fix(authors): keep non-latin letters when matching enrichment candidates (#1615)
* 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>
2026-09-30 12:15:21 -06:00
neon 09e4f1b5bd chore(deps): update minor and patch dependencies
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.
2026-09-30 00:57:24 +00:00
neon d66f853cad feat(settings): add read-aloud filename token
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
2026-09-29 01:29:25 +00:00
neon acce1e15ac fix(server): survive database connection loss
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
2026-09-29 00:37:44 +00:00
neon 7e3bb1892e fix(annotations): clear KOReader state on device removal
Remove device-scoped annotation delivery state with the rest of its KOReader data so stale acknowledgements cannot block permanent deletion.

Fixes #1576
2026-09-29 00:24:27 +00:00
neon 826584e5da feat(annotations): add bounded dashboard highlights route
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.
2026-09-28 23:50:27 +00:00
neon a35e222cb2 feat(types): expose the book file id on audiobook manifest assets
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.
2026-09-28 22:38:47 +00:00
neon 236c1785d0 fix(books): stop synthesizing audiobook chapters from unknown durations
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.
2026-09-28 22:34:14 +00:00
neon 1be423fb54 test(stats): read continue-listening books from the scroller envelope
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.
2026-09-28 22:29:44 +00:00
neon 35d5a8c2d6 fix(reader): accept audiobook positions in the last second of a track
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.
2026-09-28 22:27:45 +00:00