Loading .gitlab-ci.yml +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,8 @@ include: rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"' when: always - if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_MESSAGE =~ /\[skip ci on prod\]/i' when: never - when: never - component: "$CI_SERVER_FQDN/to-be-continuous/gitlab-butler/gitlab-ci-butler@1.4" inputs: Loading bookloresync.koplugin/booklore_database.lua +88 −11 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ local LuaSettings = require("luasettings") local logger = require("logger") local Database = { VERSION = 27, -- Current database schema version VERSION = 28, -- Current database schema version db_path = nil, conn = nil, } Loading Loading @@ -716,6 +716,36 @@ Database.migrations = { -- Migration 27: Store KOReader pagecount captured with each pending session. [27] = {}, -- Migration 28: Composite index for book_id+file_path lookups and -- shelf_target column to namespace cached shelf state per target type. [28] = { [[ CREATE INDEX IF NOT EXISTS idx_book_cache_book_id_path ON book_cache(book_id, file_path); ]], [[ CREATE TABLE IF NOT EXISTS shelf_state_v2 ( shelf_id INTEGER NOT NULL, shelf_target TEXT NOT NULL DEFAULT 'regular', books_json TEXT NOT NULL, synced_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), PRIMARY KEY (shelf_id, shelf_target) ); ]], [[ INSERT OR REPLACE INTO shelf_state_v2 (shelf_id, shelf_target, books_json, synced_at) SELECT shelf_id, 'regular', books_json, synced_at FROM shelf_state; ]], [[ DROP TABLE IF EXISTS shelf_state; ]], [[ ALTER TABLE shelf_state_v2 RENAME TO shelf_state; ]], [[ CREATE INDEX IF NOT EXISTS idx_shelf_state_target_id ON shelf_state(shelf_target, shelf_id); ]], }, } -- Post-migration hooks: Lua functions run after the SQL transaction commits. Loading Loading @@ -1285,6 +1315,50 @@ function Database:getBookByBookId(book_id) return book end ---Database:getBookByBookIdInDir. function Database:getBookByBookIdInDir(book_id, dir_prefix) book_id = tonumber(book_id) local base = tostring(dir_prefix or "") if not book_id or base == "" then return nil end base = base:gsub("/+$", "") local stmt = self.conn:prepare([[ SELECT id, file_path, file_hash, book_id, title, author, last_accessed, server_pagecount FROM book_cache WHERE book_id = ? AND file_path LIKE ? LIMIT 1 ]]) if not stmt then self.plugin:logErr("BookloreSync Database: getBookByBookIdInDir - failed to prepare:", self.conn:errmsg()) return nil end stmt:bind(book_id, base .. "/%") local book = nil for row in stmt:rows() do book = { id = tonumber(row[1]), file_path = tostring(row[2]), file_hash = tostring(row[3]), book_id = row[4] and tonumber(row[4]) or nil, title = row[5] and tostring(row[5]) or nil, author = row[6] and tostring(row[6]) or nil, last_accessed = row[7] and tonumber(row[7]) or nil, server_pagecount = row[8] and tonumber(row[8]) or nil, } break end stmt:close() return book end ---Database:saveBookCache. function Database:saveBookCache(file_path, file_hash, book_id, title, author, isbn10, isbn13, server_pagecount) file_path = tostring(file_path or "") Loading Loading @@ -4851,17 +4925,18 @@ Retrieve the cached shelf state for a shelf. @return table|nil { books_json=string, synced_at=number } or nil if not cached --]] ---Database:getShelfState. function Database:getShelfState(shelf_id) function Database:getShelfState(shelf_id, shelf_target) shelf_id = tonumber(shelf_id) if not shelf_id then return nil end shelf_target = (shelf_target == "magic") and "magic" or "regular" local stmt = self.conn:prepare([[ SELECT books_json, synced_at FROM shelf_state WHERE shelf_id = ? SELECT books_json, synced_at FROM shelf_state WHERE shelf_id = ? AND shelf_target = ? ]]) if not stmt then self.plugin:logErr("BookloreSync Database: getShelfState - failed to prepare:", self.conn:errmsg()) return nil end stmt:bind(shelf_id) stmt:bind(shelf_id, shelf_target) local result = nil for row in stmt:rows() do result = { Loading @@ -4882,22 +4957,23 @@ Uses INSERT OR REPLACE so repeated calls simply overwrite the previous row. @param books_json string JSON-encoded array of book objects from the API --]] ---Database:saveShelfState. function Database:saveShelfState(shelf_id, books_json) function Database:saveShelfState(shelf_id, shelf_target, books_json) shelf_id = tonumber(shelf_id) shelf_target = (shelf_target == "magic") and "magic" or "regular" if not shelf_id or type(books_json) ~= "string" then self.plugin:logErr("BookloreSync Database: saveShelfState - invalid arguments") return false end local stmt = self.conn:prepare([[ INSERT OR REPLACE INTO shelf_state (shelf_id, books_json, synced_at) VALUES (?, ?, strftime('%s', 'now')) INSERT OR REPLACE INTO shelf_state (shelf_id, shelf_target, books_json, synced_at) VALUES (?, ?, ?, strftime('%s', 'now')) ]]) if not stmt then self.plugin:logErr("BookloreSync Database: saveShelfState - failed to prepare:", self.conn:errmsg()) return false end local ok, err = pcall(function() stmt:bind(shelf_id, books_json) stmt:bind(shelf_id, shelf_target, books_json) stmt:step() end) stmt:close() Loading @@ -4915,16 +4991,17 @@ a full pass. @param shelf_id number Shelf ID --]] ---Database:clearShelfState. function Database:clearShelfState(shelf_id) function Database:clearShelfState(shelf_id, shelf_target) shelf_id = tonumber(shelf_id) if not shelf_id then return end local stmt = self.conn:prepare("DELETE FROM shelf_state WHERE shelf_id = ?") shelf_target = (shelf_target == "magic") and "magic" or "regular" local stmt = self.conn:prepare("DELETE FROM shelf_state WHERE shelf_id = ? AND shelf_target = ?") if not stmt then self.plugin:logErr("BookloreSync Database: clearShelfState - failed to prepare:", self.conn:errmsg()) return end local ok, err = pcall(function() stmt:bind(shelf_id) stmt:bind(shelf_id, shelf_target) stmt:step() end) stmt:close() Loading bookloresync.koplugin/booklore_deletion_module.lua +5 −3 Original line number Diff line number Diff line Loading @@ -57,8 +57,10 @@ function M.new(deps) if not self.is_enabled then return end if self.booklore_username == "" or self.booklore_password == "" then return end if self.booklore_sync_source_type == "magic" then self:logInfo("BookloreSync: notifyBookloreOnDeletion - skipping shelf unassign for magic shelf source") local regular_shelf_id = tonumber(self.booklore_source_shelf_id) or tonumber(self.shelf_id) local magic_shelf_id = tonumber(self.booklore_magic_shelf_id) if not regular_shelf_id and magic_shelf_id then self:logInfo("BookloreSync: notifyBookloreOnDeletion - skipping shelf unassign for magic-only shelf config") return end Loading Loading @@ -104,7 +106,7 @@ function M.new(deps) local token_ok, token = self.api:getOrRefreshBearerToken(self.booklore_username, self.booklore_password, false) if not token_ok then error(token or "Failed to obtain auth token") end local shelf_id = tonumber(self.booklore_source_shelf_id) or self.shelf_id local shelf_id = regular_shelf_id if not shelf_id then error("No shelf_id configured - cannot unassign book from shelf") end local headers = { Loading bookloresync.koplugin/booklore_pending_sync.lua +8 −3 Original line number Diff line number Diff line Loading @@ -94,10 +94,15 @@ function module.syncPendingDeletions(self, silent) if not self.db then return 0, 0 end if self.booklore_sync_source_type == "magic" then local regular_shelf_id = tonumber(self.booklore_source_shelf_id) or tonumber(self.shelf_id) local magic_shelf_id = tonumber(self.booklore_magic_shelf_id) -- If only a magic shelf is configured, queued deletions can never be -- unassigned from a regular shelf source; clear them immediately. if not regular_shelf_id and magic_shelf_id then local deletions = self.db:getPendingDeletions() if deletions and #deletions > 0 then self:logInfo("BookloreSync: syncPendingDeletions - clearing", #deletions, "queued deletion(s) for magic shelf source") self:logInfo("BookloreSync: syncPendingDeletions - clearing", #deletions, "queued deletion(s) for magic-only shelf config") for _, deletion in ipairs(deletions) do self.db:removePendingDeletion(deletion.id) end Loading Loading @@ -148,7 +153,7 @@ function module.syncPendingDeletions(self, silent) goto del_continue end local shelf_id = tonumber(self.booklore_source_shelf_id) or self.shelf_id local shelf_id = regular_shelf_id if not shelf_id then self:logWarn("BookloreSync: syncPendingDeletions - no shelf_id configured, cannot unassign book, skipping") self.db:incrementDeletionRetry(deletion.id) Loading bookloresync.koplugin/booklore_shelf_sync.lua +265 −65 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
.gitlab-ci.yml +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,8 @@ include: rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"' when: always - if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_MESSAGE =~ /\[skip ci on prod\]/i' when: never - when: never - component: "$CI_SERVER_FQDN/to-be-continuous/gitlab-butler/gitlab-ci-butler@1.4" inputs: Loading
bookloresync.koplugin/booklore_database.lua +88 −11 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ local LuaSettings = require("luasettings") local logger = require("logger") local Database = { VERSION = 27, -- Current database schema version VERSION = 28, -- Current database schema version db_path = nil, conn = nil, } Loading Loading @@ -716,6 +716,36 @@ Database.migrations = { -- Migration 27: Store KOReader pagecount captured with each pending session. [27] = {}, -- Migration 28: Composite index for book_id+file_path lookups and -- shelf_target column to namespace cached shelf state per target type. [28] = { [[ CREATE INDEX IF NOT EXISTS idx_book_cache_book_id_path ON book_cache(book_id, file_path); ]], [[ CREATE TABLE IF NOT EXISTS shelf_state_v2 ( shelf_id INTEGER NOT NULL, shelf_target TEXT NOT NULL DEFAULT 'regular', books_json TEXT NOT NULL, synced_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), PRIMARY KEY (shelf_id, shelf_target) ); ]], [[ INSERT OR REPLACE INTO shelf_state_v2 (shelf_id, shelf_target, books_json, synced_at) SELECT shelf_id, 'regular', books_json, synced_at FROM shelf_state; ]], [[ DROP TABLE IF EXISTS shelf_state; ]], [[ ALTER TABLE shelf_state_v2 RENAME TO shelf_state; ]], [[ CREATE INDEX IF NOT EXISTS idx_shelf_state_target_id ON shelf_state(shelf_target, shelf_id); ]], }, } -- Post-migration hooks: Lua functions run after the SQL transaction commits. Loading Loading @@ -1285,6 +1315,50 @@ function Database:getBookByBookId(book_id) return book end ---Database:getBookByBookIdInDir. function Database:getBookByBookIdInDir(book_id, dir_prefix) book_id = tonumber(book_id) local base = tostring(dir_prefix or "") if not book_id or base == "" then return nil end base = base:gsub("/+$", "") local stmt = self.conn:prepare([[ SELECT id, file_path, file_hash, book_id, title, author, last_accessed, server_pagecount FROM book_cache WHERE book_id = ? AND file_path LIKE ? LIMIT 1 ]]) if not stmt then self.plugin:logErr("BookloreSync Database: getBookByBookIdInDir - failed to prepare:", self.conn:errmsg()) return nil end stmt:bind(book_id, base .. "/%") local book = nil for row in stmt:rows() do book = { id = tonumber(row[1]), file_path = tostring(row[2]), file_hash = tostring(row[3]), book_id = row[4] and tonumber(row[4]) or nil, title = row[5] and tostring(row[5]) or nil, author = row[6] and tostring(row[6]) or nil, last_accessed = row[7] and tonumber(row[7]) or nil, server_pagecount = row[8] and tonumber(row[8]) or nil, } break end stmt:close() return book end ---Database:saveBookCache. function Database:saveBookCache(file_path, file_hash, book_id, title, author, isbn10, isbn13, server_pagecount) file_path = tostring(file_path or "") Loading Loading @@ -4851,17 +4925,18 @@ Retrieve the cached shelf state for a shelf. @return table|nil { books_json=string, synced_at=number } or nil if not cached --]] ---Database:getShelfState. function Database:getShelfState(shelf_id) function Database:getShelfState(shelf_id, shelf_target) shelf_id = tonumber(shelf_id) if not shelf_id then return nil end shelf_target = (shelf_target == "magic") and "magic" or "regular" local stmt = self.conn:prepare([[ SELECT books_json, synced_at FROM shelf_state WHERE shelf_id = ? SELECT books_json, synced_at FROM shelf_state WHERE shelf_id = ? AND shelf_target = ? ]]) if not stmt then self.plugin:logErr("BookloreSync Database: getShelfState - failed to prepare:", self.conn:errmsg()) return nil end stmt:bind(shelf_id) stmt:bind(shelf_id, shelf_target) local result = nil for row in stmt:rows() do result = { Loading @@ -4882,22 +4957,23 @@ Uses INSERT OR REPLACE so repeated calls simply overwrite the previous row. @param books_json string JSON-encoded array of book objects from the API --]] ---Database:saveShelfState. function Database:saveShelfState(shelf_id, books_json) function Database:saveShelfState(shelf_id, shelf_target, books_json) shelf_id = tonumber(shelf_id) shelf_target = (shelf_target == "magic") and "magic" or "regular" if not shelf_id or type(books_json) ~= "string" then self.plugin:logErr("BookloreSync Database: saveShelfState - invalid arguments") return false end local stmt = self.conn:prepare([[ INSERT OR REPLACE INTO shelf_state (shelf_id, books_json, synced_at) VALUES (?, ?, strftime('%s', 'now')) INSERT OR REPLACE INTO shelf_state (shelf_id, shelf_target, books_json, synced_at) VALUES (?, ?, ?, strftime('%s', 'now')) ]]) if not stmt then self.plugin:logErr("BookloreSync Database: saveShelfState - failed to prepare:", self.conn:errmsg()) return false end local ok, err = pcall(function() stmt:bind(shelf_id, books_json) stmt:bind(shelf_id, shelf_target, books_json) stmt:step() end) stmt:close() Loading @@ -4915,16 +4991,17 @@ a full pass. @param shelf_id number Shelf ID --]] ---Database:clearShelfState. function Database:clearShelfState(shelf_id) function Database:clearShelfState(shelf_id, shelf_target) shelf_id = tonumber(shelf_id) if not shelf_id then return end local stmt = self.conn:prepare("DELETE FROM shelf_state WHERE shelf_id = ?") shelf_target = (shelf_target == "magic") and "magic" or "regular" local stmt = self.conn:prepare("DELETE FROM shelf_state WHERE shelf_id = ? AND shelf_target = ?") if not stmt then self.plugin:logErr("BookloreSync Database: clearShelfState - failed to prepare:", self.conn:errmsg()) return end local ok, err = pcall(function() stmt:bind(shelf_id) stmt:bind(shelf_id, shelf_target) stmt:step() end) stmt:close() Loading
bookloresync.koplugin/booklore_deletion_module.lua +5 −3 Original line number Diff line number Diff line Loading @@ -57,8 +57,10 @@ function M.new(deps) if not self.is_enabled then return end if self.booklore_username == "" or self.booklore_password == "" then return end if self.booklore_sync_source_type == "magic" then self:logInfo("BookloreSync: notifyBookloreOnDeletion - skipping shelf unassign for magic shelf source") local regular_shelf_id = tonumber(self.booklore_source_shelf_id) or tonumber(self.shelf_id) local magic_shelf_id = tonumber(self.booklore_magic_shelf_id) if not regular_shelf_id and magic_shelf_id then self:logInfo("BookloreSync: notifyBookloreOnDeletion - skipping shelf unassign for magic-only shelf config") return end Loading Loading @@ -104,7 +106,7 @@ function M.new(deps) local token_ok, token = self.api:getOrRefreshBearerToken(self.booklore_username, self.booklore_password, false) if not token_ok then error(token or "Failed to obtain auth token") end local shelf_id = tonumber(self.booklore_source_shelf_id) or self.shelf_id local shelf_id = regular_shelf_id if not shelf_id then error("No shelf_id configured - cannot unassign book from shelf") end local headers = { Loading
bookloresync.koplugin/booklore_pending_sync.lua +8 −3 Original line number Diff line number Diff line Loading @@ -94,10 +94,15 @@ function module.syncPendingDeletions(self, silent) if not self.db then return 0, 0 end if self.booklore_sync_source_type == "magic" then local regular_shelf_id = tonumber(self.booklore_source_shelf_id) or tonumber(self.shelf_id) local magic_shelf_id = tonumber(self.booklore_magic_shelf_id) -- If only a magic shelf is configured, queued deletions can never be -- unassigned from a regular shelf source; clear them immediately. if not regular_shelf_id and magic_shelf_id then local deletions = self.db:getPendingDeletions() if deletions and #deletions > 0 then self:logInfo("BookloreSync: syncPendingDeletions - clearing", #deletions, "queued deletion(s) for magic shelf source") self:logInfo("BookloreSync: syncPendingDeletions - clearing", #deletions, "queued deletion(s) for magic-only shelf config") for _, deletion in ipairs(deletions) do self.db:removePendingDeletion(deletion.id) end Loading Loading @@ -148,7 +153,7 @@ function module.syncPendingDeletions(self, silent) goto del_continue end local shelf_id = tonumber(self.booklore_source_shelf_id) or self.shelf_id local shelf_id = regular_shelf_id if not shelf_id then self:logWarn("BookloreSync: syncPendingDeletions - no shelf_id configured, cannot unassign book, skipping") self.db:incrementDeletionRetry(deletion.id) Loading
bookloresync.koplugin/booklore_shelf_sync.lua +265 −65 File changed.Preview size limit exceeded, changes collapsed. Show changes