local json = require("json") local json_util = require("json.util") 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:", _) return false, { error = tostring(_) } 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) local books = json_util.InitArray({ book_data }) return self:_request("POST", "/api/sync/koreader/progress", { books = books, 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