Initial commit

This commit is contained in:
2026-09-25 15:29:54 +01:00
parent ce21e350a1
commit bd53601d8c
37 changed files with 5029 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
local JSON = require("json")
local Device = require("device")
local random = require("random")
local logger = require("logger")
local callApi = require("call_api")
local RawHttp = require("raw_http")
local Db = require("db_reader")
local Annotations = require("annotation_reader")
local Covers = require("cover_reader")
local const = require("./const")
local U = {}
local function headers(body, token, content_type)
local h = { ["Content-Type"] = content_type or "application/json" }
if body ~= nil then h["Content-Length"] = tostring(#body) end
if token and token ~= "" then h["Authorization"] = "Bearer " .. token end
return h
end
local function device_id()
local id = G_reader_settings:readSetting("device_id")
if id == nil or id == "" then
id = random.uuid()
G_reader_settings:saveSetting("device_id", id)
if G_reader_settings.flush then G_reader_settings:flush() end
end
return id
end
function U.ping(server_url)
return callApi("GET", server_url .. "/api/plugin/ping", { ["Accept"] = "application/json" }, nil)
end
function U.diagnose(server_url)
if server_url:match("^http://") then
local tcp_ok, tcp_info = RawHttp.probe(server_url .. "/api/plugin/ping", 6)
if not tcp_ok then
local host_port = ""
if tcp_info and tcp_info.host and tcp_info.port then
host_port = " " .. tostring(tcp_info.host) .. ":" .. tostring(tcp_info.port)
end
return false, {
error = "TCP connection to" .. host_port .. " failed: " .. tostring(tcp_info and tcp_info.detail or "unknown error") .. ". If kovi works on the computer itself, allow inbound TCP port 3000 (or your configured port) in the computer's firewall and make sure the reader is on the same LAN.",
stage = tcp_info and tcp_info.stage or "tcp",
}
end
end
local ok, response = U.ping(server_url)
if not ok then return false, response end
return true, {
version = response and response.version,
message = "TCP and HTTP checks passed.",
}
end
function U.pair(server_url, code, settings)
local reachable, diagnostic = U.diagnose(server_url)
if not reachable then return false, diagnostic end
local body = JSON.encode({
code = code,
device_id = device_id(),
model = Device.model,
version = const.VERSION,
})
local ok, response = callApi("POST", server_url .. "/api/plugin/pair", headers(body, nil), body)
if ok and response and response.token then
settings:setToken(response.token)
-- Pairing represents a fresh server-side sync identity. Reset incremental
-- cursors so a rebuilt kovi database receives a complete first sync.
G_reader_settings:saveSetting("kovi_sync_cursor", 0)
G_reader_settings:saveSetting("kovi_annotation_versions", {})
G_reader_settings:saveSetting("kovi_completion_sync_version", 0)
if G_reader_settings.flush then G_reader_settings:flush() end
return true, response
end
return false, response
end
function U.sync(server_url, token)
local cursor = tonumber(G_reader_settings:readSetting("kovi_sync_cursor")) or 0
local previous_versions = G_reader_settings:readSetting("kovi_annotation_versions") or {}
local needs_completion_scan = G_reader_settings:readSetting("kovi_completion_sync_version") ~= 1
if needs_completion_scan then previous_versions = {} end
local books = Db.bookData()
local stats = Db.progressData(cursor, books)
local annotation_sets, annotation_versions = Annotations.changed(previous_versions)
local body = JSON.encode({
books = books, stats = stats, annotation_sets = annotation_sets,
sync_cursor = cursor, version = const.VERSION
})
local ok, response = callApi("POST", server_url .. "/api/plugin/import", headers(body, token), body)
if not ok then return false, response end
-- Advance only after the server committed the import. If a request fails, the
-- same overlap/change set is retried on the next manual sync.
if response and response.sync_cursor then
G_reader_settings:saveSetting("kovi_sync_cursor", tonumber(response.sync_cursor) or cursor)
end
G_reader_settings:saveSetting("kovi_annotation_versions", annotation_versions)
G_reader_settings:saveSetting("kovi_completion_sync_version", 1)
if G_reader_settings.flush then G_reader_settings:flush() end
local requests = response and response.cover_requests
local uploaded = 0
if type(requests) == "table" and #requests > 0 then
local covers = Covers.extractRequested(requests)
for _, cover in ipairs(covers) do
local cover_headers = headers(cover.data, token, "image/jpeg")
cover_headers["X-Kovi-Book-MD5"] = cover.book_md5
local cover_ok, cover_response = callApi("POST", server_url .. "/api/plugin/cover", cover_headers, cover.data)
if cover_ok then
uploaded = uploaded + 1
else
local detail = cover_response and cover_response.error or "server rejected cover"
logger.warn("[kovi] Embedded cover upload failed", cover.book_md5, tostring(detail))
end
end
end
if response then
response.embedded_covers = uploaded
if uploaded > 0 then
response.message = tostring(response.message or "kovi sync complete.") .. " Uploaded " .. tostring(uploaded) .. " embedded cover" .. (uploaded == 1 and "." or "s.")
end
end
return true, response
end
return U