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 " )
2021-09-01 14:48:46 +00:00
local CalibreExtensions = require ( " extensions " )
2020-06-19 10:22:38 +00:00
local CalibreMetadata = require ( " metadata " )
2021-05-05 17:30:50 +00:00
local CalibreSearch = require ( " search " )
2020-06-19 10:22:38 +00:00
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 InputDialog = require ( " ui/widget/inputdialog " )
local InfoMessage = require ( " ui/widget/infomessage " )
local NetworkMgr = require ( " ui/network/manager " )
local UIManager = require ( " ui/uimanager " )
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local WidgetContainer = require ( " ui/widget/container/widgetcontainer " )
2021-04-10 18:07:24 +00:00
local lfs = require ( " libs/libkoreader-lfs " )
2020-06-19 10:22:38 +00:00
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
2021-09-01 14:48:46 +00:00
local extensions = CalibreExtensions : get ( )
2020-06-19 10:22:38 +00:00
local function getExtensionPathLengths ( )
local t = { }
2021-03-06 21:44:18 +00:00
for _ , v in ipairs ( extensions ) do
2020-06-19 10:22:38 +00:00
-- 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
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local CalibreWireless = WidgetContainer : extend {
2020-06-19 10:22:38 +00:00
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 ,
} ,
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
calibre = nil , -- hash
2020-06-19 10:22:38 +00:00
}
function CalibreWireless : init ( )
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
self.calibre = { }
2020-06-19 10:22:38 +00:00
-- 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
2022-12-17 21:37:23 +00:00
local _ , _ , replied_port = dgram : match ( " calibre wireless device client %(on (.-)%);(%d+),(%d+)$ " )
2020-06-19 10:22:38 +00:00
return host , replied_port
end
end
end
end
function CalibreWireless : checkCalibreServer ( host , port )
local socket = require ( " socket " )
local tcp = socket.tcp ( )
tcp : settimeout ( 5 )
-- In case of error, the method returns nil followed by a string describing the error. In case of success, the method returns 1.
2022-12-17 21:37:23 +00:00
local ok , err = tcp : connect ( host , port )
if ok then
2020-06-19 10:22:38 +00:00
tcp : close ( )
return true
end
2022-12-17 21:37:23 +00:00
return false , err
2020-06-19 10:22:38 +00:00
end
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
-- Standard JSON/control opcodes receive callback
function CalibreWireless : JSONReceiveCallback ( host , port )
-- NOTE: Closure trickery because we need a reference to *this* self *inside* the callback,
-- which will be called as a function from another object (namely, StreamMessageQueue).
local this = self
2021-04-13 22:35:20 +00:00
return function ( t )
local data = table.concat ( t )
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
this : onReceiveJSON ( data )
if not this.connect_message then
this.password_check_callback = function ( )
local msg
if this.invalid_password then
msg = _ ( " Invalid password " )
this.invalid_password = nil
this : disconnect ( )
2022-12-17 21:37:23 +00:00
logger.warn ( " invalid password, disconnecting " )
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
elseif this.disconnected_by_server then
msg = _ ( " Disconnected by calibre " )
this.disconnected_by_server = nil
2022-12-17 21:37:23 +00:00
logger.info ( " disconnected by calibre " )
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
else
msg = T ( _ ( " Connected to calibre server at %1 " ) ,
BD.ltr ( T ( " %1:%2 " , this.calibre_socket . host , this.calibre_socket . port ) ) )
2022-12-17 21:37:23 +00:00
logger.info ( " connected successfully " )
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
end
UIManager : show ( InfoMessage : new {
text = msg ,
timeout = 2 ,
} )
end
this.connect_message = true
UIManager : scheduleIn ( 1 , this.password_check_callback )
end
end
end
2020-06-19 10:22:38 +00:00
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 ,
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
receiveCallback = self : JSONReceiveCallback ( ) ,
2020-06-19 10:22:38 +00:00
}
self.calibre_socket : start ( )
self.calibre_messagequeue = UIManager : insertZMQ ( self.calibre_socket )
end
2022-12-17 21:37:23 +00:00
logger.info ( string.format ( " connecting to calibre @ %s:%s " , host , port ) )
2020-06-19 10:22:38 +00:00
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
2022-12-17 21:37:23 +00:00
local address_type , host , port , ok , err
2020-06-19 10:22:38 +00:00
if G_reader_settings : hasNot ( " calibre_wireless_url " ) then
host , port = self : find_calibre_server ( )
2022-12-17 21:37:23 +00:00
if host and port then
address_type = " discovered "
else
ok = false
err = _ ( " Couldn't discover a calibre instance on the local network " )
address_type = " unavailable "
end
2020-06-19 10:22:38 +00:00
else
local calibre_url = G_reader_settings : readSetting ( " calibre_wireless_url " )
host , port = calibre_url [ " address " ] , calibre_url [ " port " ]
2022-12-17 21:37:23 +00:00
address_type = " specified "
2020-06-19 10:22:38 +00:00
end
2022-12-17 21:37:23 +00:00
2020-06-19 10:22:38 +00:00
if host and port then
2022-12-17 21:37:23 +00:00
ok , err = self : checkCalibreServer ( host , port )
end
if not ok then
host = host or " ???? "
port = port or " ?? "
err = err or _ ( " N/A " )
logger.warn ( string.format ( " Cannot connect to %s calibre server at %s:%s (%s) " , address_type , host , port , err ) )
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Cannot connect to calibre server at %1 (%2) " ) ,
BD.ltr ( T ( " %1:%2 " , host , port ) ) , err )
} )
else
2020-06-19 10:22:38 +00:00
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
end
end
function CalibreWireless : disconnect ( )
2022-12-17 21:37:23 +00:00
logger.info ( " disconnecting from calibre " )
2020-06-19 10:22:38 +00:00
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 ( )
2021-05-05 17:30:50 +00:00
-- Assume the library content was modified, as such, invalidate our Search metadata cache.
CalibreSearch : invalidateCache ( )
2020-06-19 10:22:38 +00:00
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 " ) ,
2022-03-04 20:20:00 +00:00
id = " close " ,
2020-06-19 10:22:38 +00:00
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 ) )
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
if self : isCalibreAtLeast ( 4 , 18 , 0 ) then
2020-06-19 10:22:38 +00:00
-- 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
2021-04-13 22:35:20 +00:00
self.calibre_socket . receiveCallback = function ( t )
local data = table.concat ( t )
2020-06-19 10:22:38 +00:00
--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
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
-- switch back to JSON data receiving mode
calibre_socket.receiveCallback = calibre_device : JSONReceiveCallback ( )
-- if calibre sends multiple files there may be leftover JSON data
2020-06-19 10:22:38 +00:00
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 )
2021-04-10 18:07:24 +00:00
local inbox_dir = G_reader_settings : readSetting ( " inbox_dir " )
local path = inbox_dir .. " / " .. arg.lpath
local file_size = lfs.attributes ( path , " size " )
if not file_size then
self : sendJsonData ( " NOOP " , { } )
return
end
local file = io.open ( path , " rb " )
if not file then
self : sendJsonData ( " NOOP " , { } )
return
end
self : sendJsonData ( " OK " , { fileLength = file_size } )
while true do
local data = file : read ( 4096 )
if not data then break end
self.calibre_socket : send ( data )
end
file : close ( )
2020-06-19 10:22:38 +00:00
end
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
function CalibreWireless : isCalibreAtLeast ( x , y , z )
2020-06-19 10:22:38 +00:00
local v = self.calibre . version
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
local function semanticVersion ( a , b , c )
2020-06-19 10:22:38 +00:00
return ( ( a * 100000 ) + ( b * 1000 ) ) + c
end
Calibre: Minor QoL fixes (#7528)
* CalibreMetadata: Get rid of the now useless NULL-hunt: here, this was basically looking for `rapidjson.null` to replace them with... `rapidjson.null` :?. IIRC, that's a remnant of a quirk of the previous JSON parser (possibly even the previous, *previous* JSON parser ^^).
* CalibreSearch: Update the actually relevant NULL-hunt to make it explicit: replace JSON NULLs with Lua nils, instead of relying on an implementation detail of Lua-RapidJSON, because that detail just changed data type ;).
* UIManager: Make sure tasks scheduled during the final ZMQ callback are honored. e.g., the Calibre "Disconnect" handler. This happened to mostly work purely by chance before the event loop rework.
* Calibre: Restore a proper receiveCallback handler after receiving a book, in order not to break the "Disconnect" handler's state (and, well, get a working Disconnect handler, period ^^).
* Calibre: Unbreak metadata cache when it's initialized by a search (regression since #7159).
* Calibre: Handle UTC <-> local time conversions when checking the cache's timestamp against the Calibre metadata timestamp.
* Bump base (Unbreak CRe on Android, update RapidJSON)
2021-04-12 00:31:53 +00:00
return semanticVersion ( v [ 1 ] , v [ 2 ] , v [ 3 ] ) >= semanticVersion ( x , y , z )
2020-06-19 10:22:38 +00:00
end
return CalibreWireless