Three new What-to-sync toggles — Ratings, Book review, Reading time —
plus a manual 'Sync rating, review & time now' menu action.
Ratings mirror summary.rating (0-5) against the server's 1-10 (exact
doubling over, nearest-star back, 0 clears). Reviews mirror
summary.note as raw markdown (the review box shows it literally, the
same accepted tradeoff as notes). Reading time pushes the statistics
plugin's cumulative book total (getStatsBookStatus) — the server
watermark-converts only the delta, so repeated pushes are harmless.
KOReader's summary carries no timestamps, so sync runs on
change-detection: the plugin remembers the last values it pushed per
book (settings.meta_sync_state); a local diff means koreader changed
it (push), a server diff with an unchanged local copy means the server
moved (pull applies, writing straight into doc_settings + flush).
Pull on book open (2s behind the progress pull), push on book close
inside the existing goOnlineToRun window, and both in the manual
action (push first so a koreader edit survives the same gesture).
Server endpoints: /api/sync/koreader/{rating,review,stats}.
KOReader's bundled LuaJSON (json.require) decodes JSON null into a
truthy sentinel function instead of nil. getMetadata responses for a
newly linked book with no server-side progress carry
"progress": null, so the `if not result.progress` guard passed,
indexing the sentinel crashed KOReader outright, and the book could
not be opened ("attempt to index local 'progress' (a function value)"
at the pull handler). "annotations": null would crash
applyServerAnnotations the same way.
Strip the sentinels recursively at the decode boundary in _request so
every call site sees plain nil and existing nil-checks behave as
written. Reproduced in the KOReader flatpak runtime:
json.decode('{"a": null}') yields type(data.a) == "function".
Found while testing a newly OPDS-downloaded book that had no server
progress yet; the crash took down the whole application, not just the
plugin.
Downloading a book from OPDS and opening it for the first time left the
plugin without a cached bookhoard UUID, so it bootstrapped the book's
identity by pushing once. That push carried the device's first-page
position, which the server stored as a real progress update for a book
that could be mid-read from another source — tripping the cross-source
conflict detector and clobbering genuine progress before the first pull
could deliver it. Until the conflict was resolved on the web, the device
could not pull the correct progress at all.
- onReaderReady now always pulls first; a pull no longer delegates to a
push to learn the book's identity
- new _linkBookThenPull: resolves the UUID with a read-only
GET /api/sync/koreader/resolve?sha256= call (BookhoardAPI:resolveBook),
caches it in the document sidecar, then performs the pull — the
device's position is never transmitted
- the legacy push-bootstrap survives only as a background fallback for
books the server does not know by hash (404, sideloaded); interactive
pulls are told to push explicitly instead
Requires the server-side resolve endpoint (bookhoard feat(sync): add
read-only KOReader book resolve endpoint).
Bring the plugin to feature parity with the server's annotation sync
system. Previously annotations were push-only (and even that was broken
because the server ignored inline annotations in progress pushes).
ANNOTATION PULL (server → device):
- Add applyServerAnnotations() method called from _doGetProgress
after progress handling
- Parses annotations.highlights, annotations.notes, and
annotations.bookmarks from GetMetadata response
- Matches incoming annotations against local bookmarks by pos0
(start position) to detect existing entries
- New entries are inserted with correct KOReader bookmark format:
page, pos0, pos1, datetime, text, notes, color, chapter
- Existing entries with changed content are updated (LWW: server
version is authoritative since the server already resolved conflicts)
- Respects sync_highlights, sync_notes, sync_bookmarks toggles for
inbound sync, not just outbound
- Triggers onSortBookmarks + saveSettings on change for persistence
DELETE PROPAGATION:
- Parses annotations.deleted_highlights and annotations.deleted_bookmarks
from GetMetadata response
- Each entry contains device_sync_data (pos0, datetime, page) from the
original device push
- Matches local bookmarks by pos0 and removes them
- Deleted highlights only remove entries that have text (to avoid
removing plain bookmarks at the same position)
CLEANUP:
- Fix BookhoardAPI.lua: rename pcall return _ to err for clarity
(was functional but misleading variable name)
- Remove dead code: unused getLibrary() and syncBookmarks() API methods
- Remove sync_endpoints field (stored from registration response but
never read by any code)
Lua's json.encode produces {} for empty tables instead of [].
Use json.util.InitArray to mark tables as arrays so they encode
correctly as [] for authors, bookmarks, highlights, notes, and books.
pcall wraps socket.http.request returns as:
true, 1, status_code, headers, status_line
The old code captured '1' as the status code instead of
the actual HTTP status. Added '_' to skip the first return.