mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
joins calibre metadata search and calibre wireless connections into a single plugin search metadata changes: - search directly into calibre metadata files. - search can be performed on more than one library (configurable from a menu) - device scans now find all calibre libraries under a given root - search options can be configured from a menu. (case sensitive, find by title, author and path) - removed legacy global variables. - *option* to search from the reader - *option* to generate a cache of books for faster searches. calibre wireless connection changes: - keep track of books in a library (includes prunning books from calibre metadata if the file was deleted locally) - remove files on device from calibre - support password protected connections - FM integration: if we're in the inbox dir it will be updated each time a book is added or deleted. - disconnect when requested by calibre, available on newer calibre versions (+4.17) - remove unused opcodes. - better report of client name, version and device id - free disk space checks for all calibre versions - bump supported extensions to match what KOReader can handle. Users can override this with their own list of extensions (or from calibre, by configuring the wireless device).
643 lines
23 KiB
Lua
643 lines
23 KiB
Lua
--[[
|
|
This module implements the 'smart device app' protocol that communicates with calibre wireless server.
|
|
More details can be found at calibre/devices/smart_device_app/driver.py.
|
|
--]]
|
|
|
|
local BD = require("ui/bidi")
|
|
local CalibreMetadata = require("metadata")
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local NetworkMgr = require("ui/network/manager")
|
|
local UIManager = require("ui/uimanager")
|
|
local logger = require("logger")
|
|
local rapidjson = require("rapidjson")
|
|
local sleep = require("ffi/util").sleep
|
|
local sha = require("ffi/sha2")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local T = require("ffi/util").template
|
|
|
|
require("ffi/zeromq_h")
|
|
|
|
-- supported formats
|
|
local extensions = require("extensions")
|
|
local function getExtensionPathLengths()
|
|
local t = {}
|
|
for _, v in pairs(extensions) do
|
|
-- magic number from calibre, see
|
|
-- https://github.com/koreader/koreader/pull/6177#discussion_r430753964
|
|
t[v] = 37
|
|
end
|
|
return t
|
|
end
|
|
|
|
-- get real free space on disk or fallback to 1GB
|
|
local function getFreeSpace(dir)
|
|
return util.diskUsage(dir).available or 1024 * 1024 * 1024
|
|
end
|
|
|
|
-- update the view of the dir if we are currently browsing it.
|
|
local function updateDir(dir)
|
|
local FileManager = require("apps/filemanager/filemanager")
|
|
if FileManager:getCurrentDir() == dir then
|
|
FileManager.instance:reinit(dir)
|
|
end
|
|
end
|
|
|
|
local CalibreWireless = InputContainer:new{
|
|
id = "KOReader",
|
|
model = require("device").model,
|
|
version = require("version"):getCurrentRevision(),
|
|
-- calibre companion local port
|
|
port = 8134,
|
|
-- calibre broadcast ports used to find calibre server
|
|
broadcast_ports = {54982, 48123, 39001, 44044, 59678},
|
|
opcodes = {
|
|
NOOP = 12,
|
|
OK = 0,
|
|
ERROR = 20,
|
|
BOOK_DONE = 11,
|
|
CALIBRE_BUSY = 18,
|
|
SET_LIBRARY_INFO = 19,
|
|
DELETE_BOOK = 13,
|
|
DISPLAY_MESSAGE = 17,
|
|
FREE_SPACE = 5,
|
|
GET_BOOK_FILE_SEGMENT = 14,
|
|
GET_BOOK_METADATA = 15,
|
|
GET_BOOK_COUNT = 6,
|
|
GET_DEVICE_INFORMATION = 3,
|
|
GET_INITIALIZATION_INFO = 9,
|
|
SEND_BOOKLISTS = 7,
|
|
SEND_BOOK = 8,
|
|
SEND_BOOK_METADATA = 16,
|
|
SET_CALIBRE_DEVICE_INFO = 1,
|
|
SET_CALIBRE_DEVICE_NAME = 2,
|
|
TOTAL_SPACE = 4,
|
|
},
|
|
calibre = {},
|
|
}
|
|
|
|
function CalibreWireless:init()
|
|
-- reversed operator codes and names dictionary
|
|
self.opnames = {}
|
|
for name, code in pairs(self.opcodes) do
|
|
self.opnames[code] = name
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:find_calibre_server()
|
|
local socket = require("socket")
|
|
local udp = socket.udp4()
|
|
udp:setoption("broadcast", true)
|
|
udp:setsockname("*", 8134)
|
|
udp:settimeout(3)
|
|
for _, port in ipairs(self.broadcast_ports) do
|
|
-- broadcast anything to calibre ports and listen to the reply
|
|
local _, err = udp:sendto("hello", "255.255.255.255", port)
|
|
if not err then
|
|
local dgram, host = udp:receivefrom()
|
|
if dgram and host then
|
|
-- replied diagram has greet message from calibre and calibre hostname
|
|
-- calibre opds port and calibre socket port we will later connect to
|
|
local _, _, _, replied_port = dgram:match("(.-)%(on (.-)%);(.-),(.-)$")
|
|
return host, replied_port
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:checkCalibreServer(host, port)
|
|
local socket = require("socket")
|
|
local tcp = socket.tcp()
|
|
tcp:settimeout(5)
|
|
local client = tcp:connect(host, port)
|
|
-- In case of error, the method returns nil followed by a string describing the error. In case of success, the method returns 1.
|
|
if client then
|
|
tcp:close()
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CalibreWireless:initCalibreMQ(host, port)
|
|
local StreamMessageQueue = require("ui/message/streammessagequeue")
|
|
if self.calibre_socket == nil then
|
|
self.calibre_socket = StreamMessageQueue:new{
|
|
host = host,
|
|
port = port,
|
|
receiveCallback = function(data)
|
|
self:onReceiveJSON(data)
|
|
if not self.connect_message then
|
|
self.password_check_callback = function()
|
|
local msg
|
|
if self.invalid_password then
|
|
msg = _("Invalid password")
|
|
self.invalid_password = nil
|
|
self:disconnect()
|
|
elseif self.disconnected_by_server then
|
|
msg = _("Disconnected by calibre")
|
|
self.disconnected_by_server = nil
|
|
else
|
|
msg = T(_("Connected to calibre server at %1"),
|
|
BD.ltr(T("%1:%2", host, port)))
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg,
|
|
timeout = 2,
|
|
})
|
|
end
|
|
self.connect_message = true
|
|
UIManager:scheduleIn(1, self.password_check_callback)
|
|
if self.failed_connect_callback then
|
|
--don't disconnect if we connect in 10 seconds
|
|
UIManager:unschedule(self.failed_connect_callback)
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
self.calibre_socket:start()
|
|
self.calibre_messagequeue = UIManager:insertZMQ(self.calibre_socket)
|
|
end
|
|
logger.info("connected to calibre", host, port)
|
|
end
|
|
|
|
-- will callback initCalibreMQ if inbox is confirmed to be set
|
|
function CalibreWireless:setInboxDir(host, port)
|
|
local calibre_device = self
|
|
require("ui/downloadmgr"):new{
|
|
onConfirm = function(inbox)
|
|
local driver = CalibreMetadata:getDeviceInfo(inbox, "device_name")
|
|
local warning = function()
|
|
if not driver then return end
|
|
return not driver:lower():match("koreader") and not driver:lower():match("folder")
|
|
end
|
|
local save_and_resume = function()
|
|
logger.info("set inbox directory", inbox)
|
|
G_reader_settings:saveSetting("inbox_dir", inbox)
|
|
if host and port then
|
|
calibre_device:initCalibreMQ(host, port)
|
|
end
|
|
end
|
|
-- probably not a good idea to mix calibre drivers because
|
|
-- their default settings usually don't match (lpath et al)
|
|
if warning() then
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_([[This folder is already initialized as a %1.
|
|
|
|
Mixing calibre libraries is not recommended unless you know what you're doing.
|
|
|
|
Do you want to continue? ]]), driver),
|
|
|
|
ok_text = _("Continue"),
|
|
ok_callback = function()
|
|
save_and_resume()
|
|
end,
|
|
})
|
|
else
|
|
save_and_resume()
|
|
end
|
|
end,
|
|
}:chooseDir()
|
|
end
|
|
|
|
function CalibreWireless:connect()
|
|
self.connect_message = false
|
|
local host, port
|
|
if G_reader_settings:hasNot("calibre_wireless_url") then
|
|
host, port = self:find_calibre_server()
|
|
else
|
|
local calibre_url = G_reader_settings:readSetting("calibre_wireless_url")
|
|
host, port = calibre_url["address"], calibre_url["port"]
|
|
if not self:checkCalibreServer(host, port) then
|
|
host = nil
|
|
else
|
|
self.failed_connect_callback = function()
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Cannot connect to calibre server."),
|
|
})
|
|
self:disconnect()
|
|
end
|
|
-- wait 10 seconds to connect to calibre
|
|
UIManager:scheduleIn(10, self.failed_connect_callback)
|
|
end
|
|
end
|
|
if host and port then
|
|
local inbox_dir = G_reader_settings:readSetting("inbox_dir")
|
|
if inbox_dir then
|
|
CalibreMetadata:init(inbox_dir)
|
|
self:initCalibreMQ(host, port)
|
|
else
|
|
self:setInboxDir(host, port)
|
|
end
|
|
elseif not NetworkMgr:isConnected() then
|
|
NetworkMgr:promptWifiOn()
|
|
else
|
|
logger.info("cannot connect to calibre server")
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Cannot connect to calibre server."),
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:disconnect()
|
|
logger.info("disconnect from calibre")
|
|
self.connect_message = false
|
|
self.calibre_socket:stop()
|
|
UIManager:removeZMQ(self.calibre_messagequeue)
|
|
self.calibre_socket = nil
|
|
self.calibre_messagequeue = nil
|
|
CalibreMetadata:clean()
|
|
end
|
|
|
|
function CalibreWireless:reconnect()
|
|
-- to use when something went wrong and we aren't in sync with calibre
|
|
sleep(1)
|
|
self:disconnect()
|
|
sleep(1)
|
|
self:connect()
|
|
end
|
|
|
|
function CalibreWireless:onReceiveJSON(data)
|
|
self.buffer = (self.buffer or "") .. (data or "")
|
|
--logger.info("data buffer", self.buffer)
|
|
-- messages from calibre stream socket are encoded in JSON strings like this
|
|
-- 34[0, {"key0":value, "key1": value}]
|
|
-- the JSON string has a leading length string field followed by the actual
|
|
-- JSON data in which the first element is always the operator code which can
|
|
-- be looked up in the opnames dictionary
|
|
while self.buffer ~= nil do
|
|
--logger.info("buffer", self.buffer)
|
|
local index = self.buffer:find('%[') or 1
|
|
local size = tonumber(self.buffer:sub(1, index - 1))
|
|
local json_data
|
|
if size and #self.buffer >= index - 1 + size then
|
|
json_data = self.buffer:sub(index, index - 1 + size)
|
|
--logger.info("json_data", json_data)
|
|
-- reset buffer to nil if all buffer is copied out to json data
|
|
self.buffer = self.buffer:sub(index + size)
|
|
--logger.info("new buffer", self.buffer)
|
|
-- data is not complete which means there are still missing data not received
|
|
else
|
|
return
|
|
end
|
|
local json, err = rapidjson.decode(json_data)
|
|
if json then
|
|
--logger.dbg("received json table", json)
|
|
local opcode = json[1]
|
|
local arg = json[2]
|
|
if self.opnames[opcode] == 'GET_INITIALIZATION_INFO' then
|
|
self:getInitInfo(arg)
|
|
elseif self.opnames[opcode] == 'GET_DEVICE_INFORMATION' then
|
|
self:getDeviceInfo(arg)
|
|
elseif self.opnames[opcode] == 'SET_CALIBRE_DEVICE_INFO' then
|
|
self:setCalibreInfo(arg)
|
|
elseif self.opnames[opcode] == 'FREE_SPACE' then
|
|
self:getFreeSpace(arg)
|
|
elseif self.opnames[opcode] == 'SET_LIBRARY_INFO' then
|
|
self:setLibraryInfo(arg)
|
|
elseif self.opnames[opcode] == 'GET_BOOK_COUNT' then
|
|
self:getBookCount(arg)
|
|
elseif self.opnames[opcode] == 'SEND_BOOK' then
|
|
self:sendBook(arg)
|
|
elseif self.opnames[opcode] == 'DELETE_BOOK' then
|
|
self:deleteBook(arg)
|
|
elseif self.opnames[opcode] == 'GET_BOOK_FILE_SEGMENT' then
|
|
self:sendToCalibre(arg)
|
|
elseif self.opnames[opcode] == 'DISPLAY_MESSAGE' then
|
|
self:serverFeedback(arg)
|
|
elseif self.opnames[opcode] == 'NOOP' then
|
|
self:noop(arg)
|
|
end
|
|
else
|
|
logger.warn("failed to decode json data", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:sendJsonData(opname, data)
|
|
local json, err = rapidjson.encode(rapidjson.array({self.opcodes[opname], data}))
|
|
if json then
|
|
-- length of json data should be before the real json data
|
|
self.calibre_socket:send(tostring(#json)..json)
|
|
else
|
|
logger.warn("failed to encode json data", err)
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:getInitInfo(arg)
|
|
logger.dbg("GET_INITIALIZATION_INFO", arg)
|
|
local s = ""
|
|
for i, v in ipairs(arg.calibre_version) do
|
|
if i == #arg.calibre_version then
|
|
s = s .. v
|
|
else
|
|
s = s .. v .. "."
|
|
end
|
|
end
|
|
self.calibre.version = arg.calibre_version
|
|
self.calibre.version_string = s
|
|
local getPasswordHash = function()
|
|
local password = G_reader_settings:readSetting("calibre_wireless_password")
|
|
local challenge = arg.passwordChallenge
|
|
if password and challenge then
|
|
return sha.sha1(password..challenge)
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
|
|
local init_info = {
|
|
appName = self.id,
|
|
acceptedExtensions = extensions,
|
|
cacheUsesLpaths = true,
|
|
canAcceptLibraryInfo = true,
|
|
canDeleteMultipleBooks = true,
|
|
canReceiveBookBinary = true,
|
|
canSendOkToSendbook = true,
|
|
canStreamBooks = true,
|
|
canStreamMetadata = true,
|
|
canUseCachedMetadata = true,
|
|
ccVersionNumber = self.version,
|
|
coverHeight = 240,
|
|
deviceKind = self.model,
|
|
deviceName = T("%1 (%2)", self.id, self.model),
|
|
extensionPathLengths = getExtensionPathLengths(),
|
|
passwordHash = getPasswordHash(),
|
|
maxBookContentPacketLen = 4096,
|
|
useUuidFileNames = false,
|
|
versionOK = true,
|
|
}
|
|
self:sendJsonData('OK', init_info)
|
|
end
|
|
|
|
function CalibreWireless:setPassword()
|
|
local function passwordCheck(p)
|
|
local t = type(p)
|
|
if t == "number" or (t == "string" and p:match("%S")) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
local password_dialog
|
|
password_dialog = InputDialog:new{
|
|
title = _("Set a password for calibre wireless server"),
|
|
input = G_reader_settings:readSetting("calibre_wireless_password") or "",
|
|
buttons = {{
|
|
{
|
|
text = _("Cancel"),
|
|
callback = function()
|
|
UIManager:close(password_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Set password"),
|
|
callback = function()
|
|
local pass = password_dialog:getInputText()
|
|
if passwordCheck(pass) then
|
|
G_reader_settings:saveSetting("calibre_wireless_password", pass)
|
|
else
|
|
G_reader_settings:delSetting("calibre_wireless_password")
|
|
end
|
|
UIManager:close(password_dialog)
|
|
end,
|
|
},
|
|
}},
|
|
}
|
|
UIManager:show(password_dialog)
|
|
password_dialog:onShowKeyboard()
|
|
end
|
|
|
|
function CalibreWireless:getDeviceInfo(arg)
|
|
logger.dbg("GET_DEVICE_INFORMATION", arg)
|
|
local device_info = {
|
|
device_info = {
|
|
device_store_uuid = CalibreMetadata.drive.device_store_uuid,
|
|
device_name = T("%1 (%2)", self.id, self.model),
|
|
},
|
|
version = self.version,
|
|
device_version = self.version,
|
|
}
|
|
self:sendJsonData('OK', device_info)
|
|
end
|
|
|
|
function CalibreWireless:setCalibreInfo(arg)
|
|
logger.dbg("SET_CALIBRE_DEVICE_INFO", arg)
|
|
CalibreMetadata:saveDeviceInfo(arg)
|
|
self:sendJsonData('OK', {})
|
|
end
|
|
|
|
function CalibreWireless:getFreeSpace(arg)
|
|
logger.dbg("FREE_SPACE", arg)
|
|
local free_space = {
|
|
free_space_on_device = getFreeSpace(G_reader_settings:readSetting("inbox_dir")),
|
|
}
|
|
self:sendJsonData('OK', free_space)
|
|
end
|
|
|
|
function CalibreWireless:setLibraryInfo(arg)
|
|
logger.dbg("SET_LIBRARY_INFO", arg)
|
|
self:sendJsonData('OK', {})
|
|
end
|
|
|
|
function CalibreWireless:getBookCount(arg)
|
|
logger.dbg("GET_BOOK_COUNT", arg)
|
|
local books = {
|
|
willStream = true,
|
|
willScan = true,
|
|
count = #CalibreMetadata.books,
|
|
}
|
|
self:sendJsonData('OK', books)
|
|
for index, _ in ipairs(CalibreMetadata.books) do
|
|
local book = CalibreMetadata:getBookId(index)
|
|
logger.dbg(string.format("sending book id %d/%d", index, #CalibreMetadata.books))
|
|
self:sendJsonData('OK', book)
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:noop(arg)
|
|
logger.dbg("NOOP", arg)
|
|
-- calibre wants to close the socket, time to disconnect
|
|
if arg.ejecting then
|
|
self:sendJsonData('OK', {})
|
|
self.disconnected_by_server = true
|
|
self:disconnect()
|
|
return
|
|
end
|
|
-- calibre announces the count of books that need more metadata
|
|
if arg.count then
|
|
self.pending = arg.count
|
|
self.current = 1
|
|
return
|
|
end
|
|
-- calibre requests more metadata for a book by its index
|
|
if arg.priKey then
|
|
local book = CalibreMetadata:getBookMetadata(arg.priKey)
|
|
logger.dbg(string.format("sending book metadata %d/%d", self.current, self.pending))
|
|
self:sendJsonData('OK', book)
|
|
if self.current == self.pending then
|
|
self.current = nil
|
|
self.pending = nil
|
|
return
|
|
end
|
|
self.current = self.current + 1
|
|
return
|
|
end
|
|
-- keep-alive NOOP
|
|
self:sendJsonData('OK', {})
|
|
end
|
|
|
|
function CalibreWireless:sendBook(arg)
|
|
logger.dbg("SEND_BOOK", arg)
|
|
local inbox_dir = G_reader_settings:readSetting("inbox_dir")
|
|
local filename = inbox_dir .. "/" .. arg.lpath
|
|
local fits = getFreeSpace(inbox_dir) >= (arg.length + 128 * 1024)
|
|
local to_write_bytes = arg.length
|
|
local calibre_device = self
|
|
local calibre_socket = self.calibre_socket
|
|
local outfile
|
|
if fits then
|
|
logger.dbg("write to file", filename)
|
|
util.makePath((util.splitFilePathName(filename)))
|
|
outfile = io.open(filename, "wb")
|
|
else
|
|
local msg = T(_("Can't receive file %1/%2: %3\nNo space left on device"),
|
|
arg.thisBook + 1, arg.totalBooks, BD.filepath(filename))
|
|
if self:isCalibreAtLeast(4,18,0) then
|
|
-- report the error back to calibre
|
|
self:sendJsonData('ERROR', {message = msg})
|
|
return
|
|
else
|
|
-- report the error in the client
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg,
|
|
timeout = 2,
|
|
})
|
|
self.error_on_copy = true
|
|
end
|
|
end
|
|
-- switching to raw data receiving mode
|
|
self.calibre_socket.receiveCallback = function(data)
|
|
--logger.info("receive file data", #data)
|
|
--logger.info("Memory usage KB:", collectgarbage("count"))
|
|
local to_write_data = data:sub(1, to_write_bytes)
|
|
if fits then
|
|
outfile:write(to_write_data)
|
|
end
|
|
to_write_bytes = to_write_bytes - #to_write_data
|
|
if to_write_bytes == 0 then
|
|
if fits then
|
|
-- close file as all file data is received and written to local storage
|
|
outfile:close()
|
|
logger.dbg("complete writing file", filename)
|
|
-- add book to local database/table
|
|
CalibreMetadata:addBook(arg.metadata)
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Received file %1/%2: %3"),
|
|
arg.thisBook + 1, arg.totalBooks, BD.filepath(filename)),
|
|
timeout = 2,
|
|
})
|
|
CalibreMetadata:saveBookList()
|
|
updateDir(inbox_dir)
|
|
end
|
|
-- switch to JSON data receiving mode
|
|
calibre_socket.receiveCallback = function(json_data)
|
|
calibre_device:onReceiveJSON(json_data)
|
|
end
|
|
-- if calibre sends multiple files there may be left JSON data
|
|
calibre_device.buffer = data:sub(#to_write_data + 1) or ""
|
|
--logger.info("device buffer", calibre_device.buffer)
|
|
if calibre_device.buffer ~= "" then
|
|
UIManager:scheduleIn(0.1, function()
|
|
-- since data is already copied to buffer
|
|
-- onReceiveJSON parameter should be nil
|
|
calibre_device:onReceiveJSON()
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
self:sendJsonData('OK', {})
|
|
-- end of the batch
|
|
if (arg.thisBook + 1) == arg.totalBooks then
|
|
if not self.error_on_copy then return end
|
|
self.error_on_copy = nil
|
|
UIManager:show(ConfirmBox:new{
|
|
text = T(_("Insufficient disk space.\n\ncalibre %1 will report all books as in device. This might lead to errors. Please reconnect to get updated info"),
|
|
self.calibre.version_string),
|
|
ok_text = _("Reconnect"),
|
|
ok_callback = function()
|
|
-- send some info to avoid harmless but annoying exceptions in calibre
|
|
self:getFreeSpace()
|
|
self:getBookCount()
|
|
-- scheduled because it blocks!
|
|
UIManager:scheduleIn(1, function()
|
|
self:reconnect()
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:deleteBook(arg)
|
|
logger.dbg("DELETE_BOOK", arg)
|
|
self:sendJsonData('OK', {})
|
|
local inbox_dir = G_reader_settings:readSetting("inbox_dir")
|
|
if not inbox_dir then return end
|
|
-- remove all books requested by calibre
|
|
local titles = ""
|
|
for i, v in ipairs(arg.lpaths) do
|
|
local book_uuid, index = CalibreMetadata:getBookUuid(v)
|
|
if not index then
|
|
logger.warn("requested to delete a book no longer on device", arg.lpaths[i])
|
|
else
|
|
titles = titles .. "\n" .. CalibreMetadata.books[index].title
|
|
util.removeFile(inbox_dir.."/"..v)
|
|
CalibreMetadata:removeBook(v)
|
|
end
|
|
self:sendJsonData('OK', { uuid = book_uuid })
|
|
-- do things once at the end of the batch
|
|
if i == #arg.lpaths then
|
|
local msg
|
|
if i == 1 then
|
|
msg = T(_("Deleted file: %1"), BD.filepath(arg.lpaths[1]))
|
|
else
|
|
msg = T(_("Deleted %1 files in %2:\n %3"),
|
|
#arg.lpaths, BD.filepath(inbox_dir), titles)
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = msg,
|
|
timeout = 2,
|
|
})
|
|
CalibreMetadata:saveBookList()
|
|
updateDir(inbox_dir)
|
|
end
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:serverFeedback(arg)
|
|
logger.dbg("DISPLAY_MESSAGE", arg)
|
|
-- here we only care about password errors
|
|
if arg.messageKind == 1 then
|
|
self.invalid_password = true
|
|
end
|
|
end
|
|
|
|
function CalibreWireless:sendToCalibre(arg)
|
|
logger.dbg("GET_BOOK_FILE_SEGMENT", arg)
|
|
-- not implemented yet, we just send an invalid opcode to raise a control error in calibre.
|
|
-- If we don't do this calibre will wait *a lot* for the file(s)
|
|
self:sendJsonData('NOOP', {})
|
|
end
|
|
|
|
function CalibreWireless:isCalibreAtLeast(x,y,z)
|
|
local v = self.calibre.version
|
|
local function semanticVersion(a,b,c)
|
|
return ((a * 100000) + (b * 1000)) + c
|
|
end
|
|
return semanticVersion(v[1],v[2],v[3]) >= semanticVersion(x,y,z)
|
|
end
|
|
|
|
return CalibreWireless
|