Commit 4cb83a6e authored by WorldTeacher's avatar WorldTeacher
Browse files

feat(shelf-sync): allow use of both shelf types together

parent 28476764
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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:
+88 −11
Original line number Diff line number Diff line
@@ -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,
}
@@ -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.
@@ -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 "")
@@ -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 = {
@@ -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()
@@ -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()
+5 −3
Original line number Diff line number Diff line
@@ -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

@@ -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 = {
+8 −3
Original line number Diff line number Diff line
@@ -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
@@ -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)
+265 −65

File changed.

Preview size limit exceeded, changes collapsed.

Loading