From 9d7b0f689f115a1fcbf503591e98e28191db436e Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 29 Jul 2026 14:49:41 -0400 Subject: [PATCH] feat: implement bidirectional annotation sync and delete propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- BookhoardAPI.lua | 16 ++----- main.lua | 119 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/BookhoardAPI.lua b/BookhoardAPI.lua index d8f3a0c..b5208bc 100644 --- a/BookhoardAPI.lua +++ b/BookhoardAPI.lua @@ -60,23 +60,23 @@ function BookhoardAPI:_request(method, path, body, timeouts) socketutil:set_timeout(timeouts[1], timeouts[2]) - local ok, _, code + local ok, err, code if url:match("^https://") then if ssl_ok then - ok, _, code = pcall(ssl_https.request, request) + ok, err, code = pcall(ssl_https.request, request) else socketutil:reset_timeout() return false, { error = "HTTPS not supported on this device" } end else - ok, _, code = pcall(http.request, request) + ok, err, code = pcall(http.request, request) end socketutil:reset_timeout() if not ok then - logger.warn("BookhoardAPI: request failed:", _) - return false, { error = tostring(_) } + logger.warn("BookhoardAPI: request failed:", err) + return false, { error = tostring(err) } end local response_body = table.concat(sink) @@ -130,12 +130,6 @@ function BookhoardAPI:getMetadata(uuid) return self:_request("GET", "/api/sync/koreader/metadata/" .. uuid, nil, PROGRESS_TIMEOUTS) end -function BookhoardAPI:getLibrary() - return self:_request("GET", "/api/sync/koreader/library", nil, PROGRESS_TIMEOUTS) -end -function BookhoardAPI:syncBookmarks(data) - return self:_request("POST", "/api/sync/koreader/bookmarks", data, PROGRESS_TIMEOUTS) -end return BookhoardAPI diff --git a/main.lua b/main.lua index f021548..20e67e2 100644 --- a/main.lua +++ b/main.lua @@ -68,7 +68,6 @@ Bookhoard.default_settings = { sync_highlights = true, sync_notes = true, sync_mode = SYNC_MODE.IMMEDIATE, - sync_endpoints = nil, } function Bookhoard:init() @@ -212,7 +211,6 @@ function Bookhoard:buildMainMenu() ok_callback = function() self.settings.auth_token = nil self.settings.device_id = nil - self.settings.sync_endpoints = nil self.settings.auto_sync = false G_reader_settings:saveSetting(self.settings_key, self.settings) self:registerEvents() @@ -623,7 +621,6 @@ function Bookhoard:startRegistrationPoll() self.registration_id = nil self.settings.auth_token = result.auth_token self.settings.device_id = result.device_id and tostring(result.device_id) or nil - self.settings.sync_endpoints = result.sync_endpoints G_reader_settings:saveSetting(self.settings_key, self.settings) self:registerEvents() self:setupOPDS() @@ -1039,6 +1036,10 @@ function Bookhoard:_doGetProgress(interactive) return end + if result.annotations then + self:applyServerAnnotations(result.annotations) + end + local progress = result.progress local percentage = self:getLastPercent() local server_percentage = progress.percentage or 0 @@ -1100,6 +1101,118 @@ function Bookhoard:_doGetProgress(interactive) end) end +function Bookhoard:applyServerAnnotations(annotations) + if not self.ui.bookmark or not self.ui.bookmark.bookmarks then + return + end + if not annotations then return end + + local local_bms = self.ui.bookmark.bookmarks + local changed = false + local has_pages = self.ui.document.info.has_pages + + local function findLocalByPos0(pos0) + for i, bm in ipairs(local_bms) do + if bm.pos0 == pos0 then + return i + end + end + return nil + end + + local function makePageFromPos(pos0) + if has_pages then + local page = tonumber(pos0:match("(%d+)$")) + return page or pos0 + end + return pos0 + end + + local function addOrUpdate(server_entry, has_text) + local pos0 = server_entry.pos0 or "" + if pos0 == "" then return end + local idx = findLocalByPos0(pos0) + if idx then + local bm = local_bms[idx] + local srv_text = server_entry.text or "" + local srv_notes = server_entry.notes or "" + if (bm.text or "") ~= srv_text or (bm.notes or "") ~= srv_notes then + bm.text = srv_text + bm.notes = srv_notes + if server_entry.color then bm.color = server_entry.color end + changed = true + end + else + local entry = { + page = makePageFromPos(pos0), + pos0 = pos0, + pos1 = server_entry.pos1 or pos0, + datetime = server_entry.datetime or "", + text = server_entry.text or "", + notes = server_entry.notes or "", + } + if server_entry.color then entry.color = server_entry.color end + if server_entry.chapter then entry.chapter = server_entry.chapter end + table.insert(local_bms, entry) + changed = true + end + end + + if self.settings.sync_highlights and annotations.highlights then + for _, hl in ipairs(annotations.highlights) do + addOrUpdate(hl, true) + end + end + + if self.settings.sync_notes and annotations.notes then + for _, note in ipairs(annotations.notes) do + addOrUpdate(note, true) + end + end + + if self.settings.sync_bookmarks and annotations.bookmarks then + for _, bm in ipairs(annotations.bookmarks) do + addOrUpdate(bm, false) + end + end + + if annotations.deleted_highlights then + for _, del in ipairs(annotations.deleted_highlights) do + local pos0 = del.pos0 or "" + if pos0 ~= "" then + local idx = findLocalByPos0(pos0) + if idx and (local_bms[idx].text or "") ~= "" then + table.remove(local_bms, idx) + changed = true + end + end + end + end + + if annotations.deleted_bookmarks then + for _, del in ipairs(annotations.deleted_bookmarks) do + local pos0 = del.pos0 or "" + if pos0 ~= "" then + local idx = findLocalByPos0(pos0) + if idx then + table.remove(local_bms, idx) + changed = true + end + end + end + end + + if changed then + if self.ui.bookmark.onSortBookmarks then + self.ui.bookmark:onSortBookmarks() + end + if self.ui.saveSettings then + self.ui:saveSettings() + end + logger.dbg("Bookhoard: annotations updated from server") + end +end + function Bookhoard:_showSyncedMessage() UIManager:show(InfoMessage:new{ text = _("Progress has been synchronized."),