2020-06-19 10:22:38 +00:00
--[[
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 " )
2020-12-14 23:46:38 +00:00
local Device = require ( " device " )
2020-09-15 18:39:32 +00:00
local FFIUtil = require ( " ffi/util " )
2020-06-19 10:22:38 +00:00
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 sha = require ( " ffi/sha2 " )
local util = require ( " util " )
local _ = require ( " gettext " )
2020-09-15 18:39:32 +00:00
local T = FFIUtil.template
2020-06-19 10:22:38 +00:00
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
[RFC] Pagination UI shenanigans (#7335)
* Menu/KeyValuePage/ReaderGoTo: Unify the dialogs. (Generally, "Enter page number" as title, and "Go to page" as OK button).
* Allow *tapping* on pagination buttons, too. Added spacers around the text to accommodate for that.
* Disable input handlers when <= 1 pages, while still printing the label in black.
* Always display both the label and the chevrons, even on single page content. (Menu being an exception, because it can handle showing no content at all, in which case we hide the chevrons).
* KVP: Tweak the pagination buttons layout in order to have consistent centering, regardless of whether the return arrow is enabled or not. (Also, match Menu's layout, more or less).
* Menu: Minor layout tweaks to follow the KVP tweaks above. Fixes, among possibly other things, buttons in (non-FM) "List" menus overlapping the final entry (e.g., OPDS), and popout menus with a border being misaligned (e.g., Calibre, Find a file).
* CalendarView: Minor layout tweaks to follow the KVP tweaks. Ensures the pagination buttons are laid out in the same way as everywhere else (they used to be a wee bit higher).
2021-02-25 04:15:23 +00:00
-- getCurrentDir() will return nil (well, nothing, technically) if there isn't an FM instance, so,
-- unless we were passed a nil, this is technically redundant.
if FileManager.instance then
FileManager.instance : reinit ( dir )
end
2020-06-19 10:22:38 +00:00
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 )
2020-12-14 23:46:38 +00:00
local force_chooser_dir
if Device : isAndroid ( ) then
force_chooser_dir = Device.home_dir
end
2020-06-19 10:22:38 +00:00
local calibre_device = self
2020-12-14 23:46:38 +00:00
2020-06-19 10:22:38 +00:00
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
2020-08-08 10:30:25 +00:00
CalibreMetadata : init ( inbox )
2020-06-19 10:22:38 +00:00
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 ,
2020-12-14 23:46:38 +00:00
} : chooseDir ( force_chooser_dir )
2020-06-19 10:22:38 +00:00
end
function CalibreWireless : connect ( )
Various Wi-Fi QoL improvements (#6424)
* Revamped most actions that require an internet connection to a new/fixed backend that allows forwarding the initial action and running it automatically once connected. (i.e., it'll allow you to set "Action when Wi-Fi is off" to "turn_on", and whatch stuff connect and do what you wanted automatically without having to re-click anywhere instead of showing you a Wi-Fi prompt and then not doing anything without any other feedback).
* Speaking of, fixed the "turn_on" beforeWifi action to, well, actually work. It's no longer marked as experimental.
* Consistently use "Wi-Fi" everywhere.
* On Kobo/Cervantes/Sony, implemented a "Kill Wi-Fi connection when inactive" system that will automatically disconnect from Wi-Fi after sustained *network* inactivity (i.e., you can keep reading, it'll eventually turn off on its own). This should be smart and flexible enough not to murder Wi-Fi while you need it, while still not keeping it uselessly on and murdering your battery.
(i.e., enable that + turn Wi-Fi on when off and enjoy never having to bother about Wi-Fi ever again).
* Made sending `NetworkConnected` / `NetworkDisconnected` events consistent (they were only being sent... sometimes, which made relying on 'em somewhat problematic).
* restoreWifiAsync is now only run when really needed (i.e., we no longer stomp on an existing working connection just for the hell of it).
* We no longer attempt to kill a bogus non-existent Wi-Fi connection when going to suspend, we only do it when it's actually needed.
* Every method of enabling Wi-Fi will now properly tear down Wi-Fi on failure, instead of leaving it in an undefined state.
* Fixed an issue in the fancy crash screen on Kobo/reMarkable that could sometime lead to the log excerpt being missing.
* Worked-around a number of sneaky issues related to low-level Wi-Fi/DHCP/DNS handling on Kobo (see the lengthy comments [below](https://github.com/koreader/koreader/pull/6424#issuecomment-663881059) for details). Fix #6421
Incidentally, this should also fix the inconsistencies experienced re: Wi-Fi behavior in Nickel when toggling between KOReader and Nickel (use NM/KFMon, and run a current FW for best results).
* For developers, this involves various cleanups around NetworkMgr and NetworkListener. Documentation is in-line, above the concerned functions.
2020-07-27 01:39:06 +00:00
if NetworkMgr : willRerunWhenConnected ( function ( ) self : connect ( ) end ) then
return
end
2020-06-19 10:22:38 +00:00
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
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
2020-12-27 13:10:06 +00:00
if self.calibre_socket then
self.calibre_socket : stop ( )
self.calibre_socket = nil
end
if self.calibre_messagequeue then
UIManager : removeZMQ ( self.calibre_messagequeue )
self.calibre_messagequeue = nil
end
2020-06-19 10:22:38 +00:00
CalibreMetadata : clean ( )
end
function CalibreWireless : reconnect ( )
-- to use when something went wrong and we aren't in sync with calibre
2020-09-15 18:39:32 +00:00
FFIUtil.sleep ( 1 )
2020-06-19 10:22:38 +00:00
self : disconnect ( )
2020-09-15 18:39:32 +00:00
FFIUtil.sleep ( 1 )
2020-06-19 10:22:38 +00:00
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 \n No 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 \n calibre %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