Initial commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user