Files
kovi/plugins/kovi.koplugin/call_api.lua
T
2026-09-25 15:29:54 +01:00

103 lines
4.3 KiB
Lua

local socketutil = require("socketutil")
local ltn12 = require("ltn12")
local logger = require("logger")
local socket = require("socket")
local http = require("socket.http")
local JSON = require("json")
local RawHttp = require("raw_http")
local const = require("./const")
local function decode_json(content)
if not content or content == "" then return {} end
local ok, decoded = pcall(JSON.decode, content)
if ok and decoded then return decoded end
return nil
end
local function format_network_error(detail, stage, host, port)
detail = tostring(detail or "network unreachable")
local where = ""
if host and port then where = " to " .. tostring(host) .. ":" .. tostring(port) end
local hint = " Check the server URL and Wi-Fi."
if stage == "tcp" then
hint = " kovi is not reachable on that TCP port from the reader. Check the host firewall and that both devices are on the same LAN."
elseif stage == "url" then
hint = " Check the server URL."
end
return false, {
error = "kovi " .. tostring(stage or "network") .. " error" .. where .. ": " .. detail .. "." .. hint,
kind = "network",
stage = stage,
detail = detail,
}
end
local function interpret(code, resp_headers, status, content)
local parsed = decode_json(content)
local numeric_code = tonumber(code)
if numeric_code and numeric_code >= 200 and numeric_code < 300 then
if parsed then return true, parsed end
return false, { error = "kovi returned a successful response that was not valid JSON.", kind = "response" }
end
logger.err("[kovi] HTTP error", code, status, content)
if parsed and parsed.error then return false, parsed end
return false, {
error = "kovi returned HTTP " .. tostring(code) .. (status and (" (" .. tostring(status) .. ")") or ""),
kind = "http",
status = numeric_code,
}
end
local function koreader_http(method, target, headers, body)
local sink = {}
local request = {
method = method,
url = target,
headers = headers,
sink = ltn12.sink.table(sink),
redirect = false,
}
if body ~= nil then request.source = ltn12.source.string(body) end
socketutil:set_timeout(socketutil.LARGE_BLOCK_TIMEOUT, socketutil.LARGE_TOTAL_TIMEOUT)
local ok, code, resp_headers, status = pcall(function()
return socket.skip(1, http.request(request))
end)
socketutil:reset_timeout()
if not ok then
return nil, nil, nil, nil, { stage = "http", detail = tostring(code) }
end
if resp_headers == nil then
return nil, nil, nil, nil, { stage = "http", detail = tostring(status or code or "network unreachable") }
end
return code, resp_headers, status, table.concat(sink), nil
end
return function(method, target, headers, body)
headers = headers or {}
headers["Accept"] = headers["Accept"] or "application/json"
headers["Accept-Encoding"] = "identity"
headers["User-Agent"] = "kovi-KOReader/" .. tostring(const.VERSION)
logger.dbg("[kovi] API", method, target)
-- LAN pairing is normally plain HTTP. Use a minimal direct TCP HTTP client first;
-- this avoids platform-specific LuaSocket HTTP quirks and gives useful TCP errors.
if target:match("^http://") then
local code, resp_headers, status, content, raw_err = RawHttp.request(method, target, headers, body, 12)
if code then return interpret(code, resp_headers, status, content) end
logger.warn("[kovi] direct HTTP failed", raw_err and raw_err.stage, raw_err and raw_err.detail)
-- Fall back to KOReader's standard HTTP stack, matching built-in plugins.
local f_code, f_headers, f_status, f_content, fallback_err = koreader_http(method, target, headers, body)
if f_code then return interpret(f_code, f_headers, f_status, f_content) end
local best = raw_err or fallback_err or { stage = "network", detail = "network unreachable" }
return format_network_error(best.detail, best.stage, best.host, best.port)
end
-- KOReader's bundled socket.http is used by built-in plugins for HTTPS URLs.
local code, resp_headers, status, content, err = koreader_http(method, target, headers, body)
if not code then return format_network_error(err and err.detail, err and err.stage) end
return interpret(code, resp_headers, status, content)
end