Initial KOReader plugin: progress sync, annotations, device registration
Full feature set: - In-plugin device registration with approval polling - Reading progress sync (push on page update/close/suspend, pull on open/resume) - Bookmark, highlight, and note sync via batch annotation collection - Configurable sync behavior (silent/prompt/disable for forward/backward) - OPDS catalog setup helper - SHA-256 book hashing with doc_settings cache - Debounced API calls with periodic push scheduling
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
local json = require("json")
|
||||
local logger = require("logger")
|
||||
local ltn12 = require("ltn12")
|
||||
local socketutil = require("socketutil")
|
||||
|
||||
local http = require("socket.http")
|
||||
local ssl_ok, ssl_https = pcall(require, "ssl.https")
|
||||
|
||||
local PROGRESS_TIMEOUTS = { 2, 5 }
|
||||
local AUTH_TIMEOUTS = { 5, 10 }
|
||||
|
||||
local BookhoardAPI = {
|
||||
server_url = nil,
|
||||
auth_token = nil,
|
||||
}
|
||||
|
||||
function BookhoardAPI:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
return o
|
||||
end
|
||||
|
||||
function BookhoardAPI:_buildHeaders()
|
||||
local headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
["Accept"] = "application/json",
|
||||
}
|
||||
if self.auth_token then
|
||||
headers["Authorization"] = "Bearer " .. self.auth_token
|
||||
end
|
||||
return headers
|
||||
end
|
||||
|
||||
function BookhoardAPI:_request(method, path, body, timeouts)
|
||||
if not self.server_url then
|
||||
return false, { error = "server URL not configured" }
|
||||
end
|
||||
|
||||
local url = self.server_url .. path
|
||||
local sink = {}
|
||||
local headers = self:_buildHeaders()
|
||||
local body_str = body and json.encode(body) or nil
|
||||
|
||||
if body_str then
|
||||
headers["Content-Length"] = tostring(#body_str)
|
||||
end
|
||||
|
||||
local request = {
|
||||
url = url,
|
||||
method = method,
|
||||
headers = headers,
|
||||
sink = ltn12.sink.table(sink),
|
||||
}
|
||||
|
||||
if body_str then
|
||||
request.source = ltn12.source.string(body_str)
|
||||
end
|
||||
|
||||
socketutil:set_timeout(timeouts[1], timeouts[2])
|
||||
|
||||
local ok, code
|
||||
if url:match("^https://") then
|
||||
if ssl_ok then
|
||||
ok, 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)
|
||||
end
|
||||
|
||||
socketutil:reset_timeout()
|
||||
|
||||
if not ok then
|
||||
logger.warn("BookhoardAPI: request failed:", code)
|
||||
return false, { error = tostring(code) }
|
||||
end
|
||||
|
||||
local response_body = table.concat(sink)
|
||||
local status = tonumber(code)
|
||||
|
||||
if status and status >= 200 and status < 300 then
|
||||
if response_body and #response_body > 0 then
|
||||
local decode_ok, data = pcall(json.decode, response_body)
|
||||
if decode_ok and type(data) == "table" then
|
||||
return true, data
|
||||
end
|
||||
return true, response_body
|
||||
end
|
||||
return true, nil
|
||||
end
|
||||
|
||||
local error_msg = "HTTP " .. tostring(code)
|
||||
if response_body and #response_body > 0 then
|
||||
local decode_ok, data = pcall(json.decode, response_body)
|
||||
if decode_ok and data and data.error then
|
||||
error_msg = data.error
|
||||
end
|
||||
end
|
||||
logger.warn("BookhoardAPI:", method, path, "→", status, error_msg)
|
||||
return false, { status = status, error = error_msg }
|
||||
end
|
||||
|
||||
function BookhoardAPI:registerDevice(device_name, device_identifier)
|
||||
return self:_request("POST", "/api/devices/register", {
|
||||
device_name = device_name,
|
||||
device_type = "koreader",
|
||||
device_identifier = device_identifier,
|
||||
}, AUTH_TIMEOUTS)
|
||||
end
|
||||
|
||||
function BookhoardAPI:checkRegistrationStatus(registration_id)
|
||||
return self:_request("POST", "/api/devices/register/status", {
|
||||
registration_id = registration_id,
|
||||
}, AUTH_TIMEOUTS)
|
||||
end
|
||||
|
||||
function BookhoardAPI:syncProgress(book_data, sync_mode)
|
||||
return self:_request("POST", "/api/sync/koreader/progress", {
|
||||
books = { book_data },
|
||||
sync_mode = sync_mode or "immediate",
|
||||
}, PROGRESS_TIMEOUTS)
|
||||
end
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user