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
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
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
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
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
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.
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.
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.
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
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
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
Keep relative file paths synchronized during manual renames and repair
stale rows during watcher and full-scan reconciliation. Derive email
attachment names from the file being streamed.
Fixes#1286
* fix(scanner): invalidate dir scan state on book deletion
Deleting a book row left its library_dir_scan_state entries intact, so the incremental scanner kept skipping the unchanged directory and never re-created the book on rescan. Invalidate the deleted books' folder paths and ancestors after deleteByIds so the next scan re-reads them and re-discovers the records.
* fix(scanner): make scan-state invalidation atomic
Prevent deleted books and concurrent scans from restoring stale directory state by versioning each library folder and coordinating writes under a database transaction.
* test(server): repair full-suite failures
Update the Book Dock expectation to the unit-record processor contract and bound the watchdog cutoff against timestamps captured around the sweep.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Saving metadata writes each audio file through a .bookorbit-write-* temp
file, which the watcher classified as content and turned into a folder
rescan of the book mid-write. The rescan read pre-write tags back over the
saved values, and an empty or failed audio read was persisted as nulls.
Closes#1164
Seed the random sort field from a per-view randomSeed on BookQuery rather
than a day counter, so a reload or a scope change draws a new order while
every page of one listing stays coherent. Callers that omit the seed keep
the daily fallback. Adds a shuffle button to the library, smart scope, and
collection toolbars when the active sort is random.
Closes#1153
Multiple audiobook editions of one title resolved to the same target path, so
enabling file rename merged or skipped every narrator but the first. Add
{narrators}, plus a :max3 modifier that yields nothing above three values, and
judge an optional group by what its placeholders render so the fallback literal
stays in the user's own pattern. The five duplicate token builders collapse into
one shared helper, which also gives audio uploads the metadata tokens they never
had. Build the shared types package before server lint, since type-aware rules
degrade to unknown on a clean checkout without it.
Refs #1108
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
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
The search params used one flag for two questions: which edition a provider should return, and
whether audiobook-only providers may run. Every caller that wanted the second forced the flag true,
so iTunes asked for entity=audiobook whenever Audible, AudNexus or Libro.fm was in the provider set.
Split the two, and derive the media type from the book's primary file so a record holding both an
ebook and an audiobook no longer searches as an audiobook in one drawer and an ebook in the other.
Closes#982
Collections opened in title order, so a curated shelf came back rearranged. Add a collectionOrder
sort field, make it the default for collection views, and offer it from a sort control that grid
and list previously did not have.
Back the ordering with a new collection_books.position sequence rather than added_at: added_at
defaults to now(), the transaction timestamp, so every book added in one request shared it and the
order fell back to books.id. Kobo collection paging had the same tie and now orders by position.
Also stop useBookWindow blanking its slots and total on every query change. Clearing them collapsed
the virtual list to zero height and back, which read as a flash on any sort, search, or filter
change.
Closes#978
Resolve {library} to the name of the library a book belongs to, so a
single KOReader device pattern can keep each library in its own subtree
on the device instead of merging every library into one tree.
The token is populated in every server-side pattern surface: KOReader
manifest and book detail device paths, upload, automatic and bulk
rename, book move (target library), download filenames, and BookDock
finalize.
* feat(metadata): add hardcover edition selection and refresh pinning
Mirrors the StoryGraph edition picker for Hardcover, but backed by real
GraphQL edition data instead of scraping. An edition can now be chosen
either from the Hardcover settings page's new "Linked books" section or
from the metadata downloader/refresh flow, and once chosen it pins
future automatic metadata refreshes to that exact edition instead of
silently re-deriving one by ISBN every time.
book_metadata.hardcover_edition_id is shared across all users, while
hardcover_book_state.hardcover_edition_id is scoped per (user, book).
Cross-writing between them is one-directional and non-destructive per
flow: the settings page always updates the acting user's own sync
state and only back-fills the shared field if it is empty; the
metadata downloader always updates the shared field and only updates
the acting user's own sync state if they already have one. The
propagation uses the existing achievement event bus rather than a
direct module import, since HardcoverModule already depends on
BookModule.
* fix(metadata): address hardcover edition selection review findings
Responds to CodeRabbit review on PR #936:
- Surface Hardcover edition-lookup failures as errors instead of an
empty list, so a provider outage cannot be mistaken for "no
editions". Updates the client to throw and the UI to show a
retryable error state.
- Validate that a manually-picked edition id actually belongs to the
book's matched Hardcover book before persisting it, rejecting
mismatches with a 400 instead of trusting an unvalidated client
input.
- Require an exact safe-integer string before parsing a propagated
hardcoverEditionId, so a value like "200abc" cannot be silently
truncated into a valid-looking edition id.
- Add durationMs to the edition-propagation failure log per the
project's logging conventions.
- Honor a pinned edition in the ISBN search() path too, not only in
lookupById(), so a refresh that falls back to search still returns
the previously chosen edition when it's present.
* test(metadata): strengthen hardcover edition-pin fallback assertions
The fallback tests for search() and lookupById() only used a
single-edition fixture, so they would have passed even if the
edition-pin filtering never ran. Use two-edition fixtures and assert
on which edition is actually selected instead.
* i18n(hardcover): use i18n keys for Linked books strings
HardcoverLinkedBooks.vue hardcoded its UI text instead of using
useI18n like its sibling components, breaking the ability to
translate the Hardcover settings page and non-English catalogs.
* fix(hardcover): harden edition selection and bound the linked-book list
Validate a picked edition against the book it belongs to rather than against a
capped, unordered list. Editions past the cap stay selectable, and two calls
can no longer disagree about which window they saw.
- Order the edition query by id and fetch one row past the cap, so truncation
is detectable instead of silent. Verified against the live Hardcover API:
both "The Name of the Wind" and "Pride and Prejudice" exceed the 50 cap.
- Report truncation for the linked-book list and the edition list, and say so
in the UI rather than presenting a partial list as complete.
- Bound and order the linked-book query. It backed a settings list with the
unbounded sync query; sync callers keep their previous unordered behaviour.
- Throw for "not connected" and "not matched" instead of returning an empty
list, which the UI could not tell apart from a genuinely empty catalogue.
- Drop release_year from the edition query. Hardcover never populates it, so
it was dead weight in the payload.
- Add a tuned --success token. The status pill hardcoded text-green-600, which
is untuned in dark mode; the light value is set for 4.5:1 on card and page.
- Expose row expansion with aria-expanded and aria-controls, announce load
failures through a live region, and keep the edition list open after a
switch so the refreshed marker lands where the user is looking.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Add index on books table for random dashboard scroller queries, batch dashboard scroller requests, instrument pg pool logging, and cover dashboard scroller batching in e2e tests.
Record how many books a series holds from Hardcover, ComicVine, RanobeDB,
Amazon and embedded ComicInfo metadata, storing it on the series row. Show
an ownership meter beside read progress on series detail, and extend gap
detection to report entries past the highest owned index instead of stopping
there. Allow the total to be set by hand in the book metadata editor.
Automated sources only ever raise the stored total so the result does not
depend on scan order; the manual field is the way to lower or clear it.
Closes#801
* fix(metadata): stop concatenating series and issue number into comicvine title
ComicVine search and auto-fill set title to a synthesized "Series #N -
Name" string instead of the issue's own name, inconsistent with how
ComicInfo.xml's <Title> is used elsewhere in the app (issue title only,
series and issue number in their own fields). title now carries just
the issue name, falling back to "<series> #<issue>" only when ComicVine
has no name for that issue.
The concatenated format is genuinely useful for telling issues apart
in the search-results list, so a new optional displayTitle field
preserves that exact UX there, populated only by the ComicVine mapper.
It's display-only and never read by any metadata-apply path, so the
list keeps its current look while the metadata that actually gets
persisted is cleanly separated.
filterAndRank now scores candidates against displayTitle when present,
falling back to title otherwise, so shortening title doesn't drop named
issues when users search by series name. Since displayTitle is only
ever set by the ComicVine mapper, this reproduces today's exact
ranking behavior for ComicVine and leaves every other provider
untouched.
Closes#841
* fix(metadata): leave comic titles unset when comicvine has no issue name
Address review feedback on the comicvine title mapping.
Issue #841 asked for the issue name alone, or nothing when comicvine has none for that issue.
Falling back to "<series> #<issue>" kept writing a synthesized title for unnamed issues, which
is the common case in a comic run, so the ComicInfo round trip only held for named issues. Make
MetadataCandidate.title optional and leave it unset instead: the pipeline already skips absent
fields, and the client already omits diff rows whose candidate value is empty, so an unnamed
issue now leaves any existing title alone. displayTitle still labels those candidates in the UI.
Comic titles no longer parse as "<series> #<issue>", which is how ComicVineProvider decided
between its precise volume-plus-issue search and the far looser general issue search. Carry
seriesName and seriesIndex on MetadataSearchParams and prefer them when the title does not
parse, so a refresh or a re-search of an already-matched comic keeps the structured path. A
title that does parse still wins, since that is a user typing an explicit target.
Also apply the display title in the diff panel's within-provider result picker, which renders
only when a provider returned several candidates and so needs the issue number most; match the
skip patterns against the same string that gets scored rather than title alone; and drop the
redundant cover alt text on the result card, where the headline already names the result.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Extend book sorting to support custom metadata field references (custom:<id>). Join custom metadata values in BookSortBuilder based on field data type, resolve active field types in BookService, and expose custom fields in BookSortBuilder UI.
Closes#829
Ensure metadata score calculations are awaited when book metadata or covers are saved or updated, and add concurrency queuing for on-demand score recalculations.
Expand audit log entries to record detailed resource metadata (such as book titles on deletion), and add rich filtering by action, actor, and resource in the audit log viewer UI.
Closes#747
Separate transient query filter rules from saved smart scope rules when evaluating series collapsing eligibility so series collapse as expected when requested in smart scopes.
Closes#780
Extend jump rails across alphabetical, temporal, and categorical sorts.
Keep large libraries responsive with bounded database buckets and exact offsets.
Refine active-state tracking, Unicode initials, localized labels, accessibility, adaptive sizing, and flat or collapsed-series navigation.
Add bounded, user-scoped temporal buckets for supported date sorts and adapt resolution to populated dates. Keep rail navigation responsive with viewport sizing, sparse-gap compression, and virtual-grid synchronization.
Provide a series-collapse toggle in the Smart Scopes view to reduce clutter from large series. Use the existing series collapse preferences logic to allow users to toggle this setting per Smart Scope, maintaining consistency with Library and Collection views.
Closes#609
Migrate the snapshot and snapshot books tables to be per-device instead
of per-user. This resolves a sync race condition where only the first
registered Kobo device for a user receives the library delta, leaving the
others empty.
Closes#314
- Add a persisted publication date while continuing to derive publication years.
- Thread full dates through metadata import, editing, filters, sorting, write-back, and sync payloads.
- Cover provider parsing, DTO validation, jump buckets, and date formatting with tests.
Closes#596
Use the user's configured download pattern to name single-book zip archives instead of defaulting to a generic 'books.zip'. Update the frontend reader cover card to download multi-track audiobooks as zip files directly.
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
Narrator names and sort names are now normalized to remove duplicate
internal whitespace and leading/trailing spaces. Names are deduplicated
case-insensitively when saved to prevent duplicate metadata entries that
are visually identical or very similar in the UI.
Closes#553
When saving metadata synchronously, if the file write operation fails, the rename operation is now skipped to avoid renaming a file that could be left in an inconsistent state.
Closes#532
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 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
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.