2020-06-19 10:22:38 +00:00
|
|
|
--[[
|
|
|
|
This module implements calibre metadata searching.
|
|
|
|
--]]
|
|
|
|
|
|
|
|
local CalibreMetadata = require("metadata")
|
|
|
|
local ConfirmBox = require("ui/widget/confirmbox")
|
|
|
|
local DataStorage = require("datastorage")
|
|
|
|
local Device = require("device")
|
2023-05-03 12:43:05 +00:00
|
|
|
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
|
2020-06-19 10:22:38 +00:00
|
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
|
|
local Menu = require("ui/widget/menu")
|
2021-01-13 10:45:16 +00:00
|
|
|
local Persist = require("persist")
|
2020-06-19 10:22:38 +00:00
|
|
|
local Screen = require("device").screen
|
|
|
|
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")
|
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 lfs = require("libs/libkoreader-lfs")
|
2020-06-19 10:22:38 +00:00
|
|
|
local logger = require("logger")
|
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 rapidjson = require("rapidjson")
|
2023-01-16 18:36:22 +00:00
|
|
|
local sort = require("sort")
|
2022-05-05 19:00:22 +00:00
|
|
|
local time = require("ui/time")
|
2023-01-16 18:36:22 +00:00
|
|
|
local util = require("util")
|
2020-06-19 10:22:38 +00:00
|
|
|
local _ = require("gettext")
|
|
|
|
local T = require("ffi/util").template
|
|
|
|
|
|
|
|
-- get root dir for disk scans
|
|
|
|
local function getDefaultRootDir()
|
|
|
|
if Device:isCervantes() or Device:isKobo() then
|
|
|
|
return "/mnt"
|
2021-01-13 10:45:16 +00:00
|
|
|
elseif Device:isEmulator() then
|
|
|
|
return lfs.currentdir()
|
2020-06-19 10:22:38 +00:00
|
|
|
else
|
|
|
|
return Device.home_dir or lfs.currentdir()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- get metadata from calibre libraries
|
|
|
|
local function getAllMetadata(t)
|
|
|
|
local books = {}
|
|
|
|
for path, enabled in pairs(t) do
|
|
|
|
if enabled and CalibreMetadata:init(path, true) then
|
|
|
|
-- calibre BQ driver reports invalid lpath
|
|
|
|
if Device:isCervantes() then
|
|
|
|
local device_name = CalibreMetadata.drive.device_name
|
|
|
|
if device_name and string.match(string.upper(device_name), "BQ") then
|
|
|
|
path = path .. "/Books"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
for _, book in ipairs(CalibreMetadata.books) do
|
2021-01-24 12:47:52 +00:00
|
|
|
book.rootpath = path
|
|
|
|
table.insert(books, #books + 1, book)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
CalibreMetadata:clean()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return books
|
|
|
|
end
|
|
|
|
|
|
|
|
-- check if a string matches a query
|
|
|
|
local function match(str, query, case_insensitive)
|
|
|
|
if query and case_insensitive then
|
|
|
|
return string.find(string.upper(str), string.upper(query))
|
|
|
|
elseif query then
|
|
|
|
return string.find(str, query)
|
|
|
|
else
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
-- get books that exactly match the search in a specific flat field (series or title)
|
|
|
|
local function getBooksByField(t, field, query)
|
2020-06-19 10:22:38 +00:00
|
|
|
local result = {}
|
|
|
|
for _, book in ipairs(t) do
|
2023-02-14 23:40:40 +00:00
|
|
|
local data = book[field]
|
|
|
|
-- We can compare nil & rapidjson.null (light userdata) to a string safely
|
|
|
|
if data == query then
|
|
|
|
table.insert(result, book)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
-- get books that exactly match the search in a specific array (tags or authors)
|
|
|
|
local function getBooksByNestedField(t, field, query)
|
2020-06-19 10:22:38 +00:00
|
|
|
local result = {}
|
|
|
|
for _, book in ipairs(t) do
|
2023-02-14 23:40:40 +00:00
|
|
|
local array = book[field]
|
|
|
|
for __, data in ipairs(array) do
|
|
|
|
if data == query then
|
2020-06-19 10:22:38 +00:00
|
|
|
table.insert(result, book)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
-- generic search in a specific flat field (series or title), matching the search criteria and their frequency
|
|
|
|
local function searchByField(t, field, query, case_insensitive)
|
2020-06-19 10:22:38 +00:00
|
|
|
local freq = {}
|
|
|
|
for _, book in ipairs(t) do
|
2023-02-14 23:40:40 +00:00
|
|
|
local data = book[field]
|
|
|
|
-- We have to make sure we only pass strings to match
|
|
|
|
if data and data ~= rapidjson.null then
|
|
|
|
if match(data, query, case_insensitive) then
|
|
|
|
freq[data] = (freq[data] or 0) + 1
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return freq
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
-- generic search in a specific array (tags or authors), matching the search criteria and their frequency
|
|
|
|
local function searchByNestedField(t, field, query, case_insensitive)
|
2020-06-19 10:22:38 +00:00
|
|
|
local freq = {}
|
|
|
|
for _, book in ipairs(t) do
|
2023-02-14 23:40:40 +00:00
|
|
|
local array = book[field]
|
|
|
|
for __, data in ipairs(array) do
|
|
|
|
if match(data, query, case_insensitive) then
|
|
|
|
freq[data] = (freq[data] or 0) + 1
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return freq
|
|
|
|
end
|
|
|
|
|
|
|
|
-- get book info as one big string with relevant metadata
|
|
|
|
local function getBookInfo(book)
|
|
|
|
-- comma separated elements from a table
|
|
|
|
local function getEntries(t)
|
|
|
|
if not t then return end
|
|
|
|
local id
|
|
|
|
for i, v in ipairs(t) do
|
|
|
|
if v ~= nil then
|
|
|
|
if i == 1 then
|
|
|
|
id = v
|
|
|
|
else
|
|
|
|
id = id .. ", " .. v
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return id
|
|
|
|
end
|
|
|
|
-- all entries can be empty, except size, which is always filled by calibre.
|
|
|
|
local title = _("Title:") .. " " .. book.title or "-"
|
|
|
|
local authors = _("Author(s):") .. " " .. getEntries(book.authors) or "-"
|
2021-01-24 12:47:52 +00:00
|
|
|
local size = _("Size:") .. " " .. util.getFriendlySize(book.size) or _("Unknown")
|
2020-06-19 10:22:38 +00:00
|
|
|
local tags = getEntries(book.tags)
|
|
|
|
if tags then
|
|
|
|
tags = _("Tags:") .. " " .. tags
|
|
|
|
end
|
|
|
|
local series
|
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 book.series and book.series ~= rapidjson.null then
|
2020-06-19 10:22:38 +00:00
|
|
|
series = _("Series:") .. " " .. book.series
|
|
|
|
end
|
|
|
|
return string.format("%s\n%s\n%s%s%s", title, authors,
|
|
|
|
tags and tags .. "\n" or "",
|
|
|
|
series and series .. "\n" or "",
|
|
|
|
size)
|
|
|
|
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
|
|
|
-- This is a singleton
|
|
|
|
local CalibreSearch = WidgetContainer:extend{
|
2020-06-19 10:22:38 +00:00
|
|
|
books = {},
|
|
|
|
libraries = {},
|
2023-01-16 18:36:22 +00:00
|
|
|
natsort_cache = {},
|
2020-06-19 10:22:38 +00:00
|
|
|
last_scan = {},
|
2023-02-14 23:40:40 +00:00
|
|
|
-- These are enabled by default
|
|
|
|
default_search_options = {
|
2020-06-19 10:22:38 +00:00
|
|
|
"cache_metadata",
|
|
|
|
"case_insensitive",
|
|
|
|
"find_by_title",
|
|
|
|
"find_by_authors",
|
2023-02-14 23:40:40 +00:00
|
|
|
},
|
|
|
|
-- These aren't
|
|
|
|
extra_search_options = {
|
|
|
|
"find_by_series",
|
|
|
|
"find_by_tag",
|
2020-06-19 10:22:38 +00:00
|
|
|
"find_by_path",
|
|
|
|
},
|
2021-01-13 10:45:16 +00:00
|
|
|
|
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
|
|
|
cache_dir = DataStorage:getDataDir() .. "/cache/calibre",
|
2021-01-13 10:45:16 +00:00
|
|
|
cache_libs = Persist:new{
|
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
|
|
|
path = DataStorage:getDataDir() .. "/cache/calibre/libraries.lua",
|
2021-01-13 10:45:16 +00:00
|
|
|
},
|
|
|
|
cache_books = Persist:new{
|
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
|
|
|
path = DataStorage:getDataDir() .. "/cache/calibre/books.dat",
|
2021-04-13 22:35:20 +00:00
|
|
|
codec = "zstd",
|
2021-01-13 10:45:16 +00:00
|
|
|
},
|
2020-06-19 10:22:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function CalibreSearch:ShowSearch()
|
|
|
|
self.search_dialog = InputDialog:new{
|
2021-04-02 15:59:29 +00:00
|
|
|
title = _("Calibre metadata search"),
|
2020-06-19 10:22:38 +00:00
|
|
|
input = self.search_value,
|
|
|
|
buttons = {
|
|
|
|
{
|
|
|
|
{
|
|
|
|
text = _("Browse series"),
|
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_value = self.search_dialog:getInputText()
|
|
|
|
self.lastsearch = "series"
|
|
|
|
self:close()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Browse tags"),
|
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_value = self.search_dialog:getInputText()
|
|
|
|
self.lastsearch = "tags"
|
|
|
|
self:close()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
2023-02-14 23:40:40 +00:00
|
|
|
{
|
|
|
|
{
|
|
|
|
text = _("Browse authors"),
|
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_value = self.search_dialog:getInputText()
|
|
|
|
self.lastsearch = "authors"
|
|
|
|
self:close()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text = _("Browse titles"),
|
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_value = self.search_dialog:getInputText()
|
|
|
|
self.lastsearch = "title"
|
|
|
|
self:close()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
2020-06-19 10:22:38 +00:00
|
|
|
{
|
|
|
|
{
|
|
|
|
text = _("Cancel"),
|
2022-03-04 20:20:00 +00:00
|
|
|
id = "close",
|
2020-06-19 10:22:38 +00:00
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_dialog:onClose()
|
|
|
|
UIManager:close(self.search_dialog)
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
-- @translators Search for books in calibre Library, via on-device metadata (as setup by Calibre's 'Send To Device').
|
2021-04-02 15:59:29 +00:00
|
|
|
text = _("Search books"),
|
2020-06-19 10:22:38 +00:00
|
|
|
enabled = true,
|
|
|
|
callback = function()
|
|
|
|
self.search_value = self.search_dialog:getInputText()
|
|
|
|
self.lastsearch = "find"
|
|
|
|
self:close()
|
|
|
|
end,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
UIManager:show(self.search_dialog)
|
|
|
|
self.search_dialog:onShowKeyboard()
|
|
|
|
end
|
|
|
|
|
|
|
|
function CalibreSearch:close()
|
|
|
|
if self.search_value then
|
|
|
|
self.search_dialog:onClose()
|
|
|
|
UIManager:close(self.search_dialog)
|
|
|
|
if string.len(self.search_value) > 0 or self.lastsearch ~= "find" then
|
|
|
|
self:find(self.lastsearch)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function CalibreSearch:onMenuHold(item)
|
|
|
|
if not item.info or item.info:len() <= 0 then return end
|
2023-05-03 12:43:05 +00:00
|
|
|
local thumbnail = FileManagerBookInfo:getCoverImage(nil, item.path)
|
2020-06-19 10:22:38 +00:00
|
|
|
local thumbwidth = math.min(240, Screen:getWidth()/3)
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
|
|
text = item.info,
|
|
|
|
image = thumbnail,
|
|
|
|
image_width = thumbwidth,
|
|
|
|
image_height = thumbwidth/2*3
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
function CalibreSearch:bookCatalog(t, option)
|
|
|
|
local catalog = {}
|
|
|
|
local series, subseries
|
|
|
|
if option and option == "series" then
|
|
|
|
series = true
|
|
|
|
end
|
|
|
|
for _, book in ipairs(t) do
|
|
|
|
local entry = {}
|
|
|
|
entry.info = getBookInfo(book)
|
|
|
|
entry.path = book.rootpath .. "/" .. book.lpath
|
2022-03-11 18:40:02 +00:00
|
|
|
if series and book.series_index then
|
2023-01-16 18:36:22 +00:00
|
|
|
local major, minor = string.format("%05.2f", book.series_index):match("([^.]+)%.([^.]+)")
|
2020-06-19 10:22:38 +00:00
|
|
|
if minor ~= "00" then
|
|
|
|
subseries = true
|
|
|
|
end
|
|
|
|
entry.text = string.format("%s.%s | %s - %s", major, minor, book.title, book.authors[1])
|
|
|
|
else
|
|
|
|
entry.text = string.format("%s - %s", book.title, book.authors[1])
|
|
|
|
end
|
|
|
|
entry.callback = function()
|
ReaderUI: Saner FM/RD lifecycle
* Ensure that going from one to the other tears down the former and
its plugins before instantiating the latter and its plugins.
UIManager: Unify Event sending & broadcasting
* Make the two behave the same way (walk the widget stack from top to
bottom), and properly handle the window stack shrinking shrinking
*and* growing.
Previously, broadcasting happened bottom-to-top and didn't really
handle the list shrinking/growing, while sending only handled the list
shrinking by a single element, and hopefully that element being the one
the event was just sent to.
These two items combined allowed us to optimize suboptimal
refresh behavior with Menu and other Menu classes when
opening/closing a document.
e.g., the "opening document" Notification is now properly regional,
and the "open last doc" option no longer flashes like a crazy person
anymore.
Plugins: Allow optimizing Menu refresh with custom menus, too.
Requires moving Menu's close_callback *after* onMenuSelect, which, eh,
probably makes sense, and is probably harmless in the grand scheme of
things.
2021-05-01 16:53:04 +00:00
|
|
|
local Event = require("ui/event")
|
|
|
|
UIManager:broadcastEvent(Event:new("SetupShowReader"))
|
|
|
|
|
|
|
|
self.search_menu:onClose()
|
|
|
|
|
2020-06-19 10:22:38 +00:00
|
|
|
local ReaderUI = require("apps/reader/readerui")
|
|
|
|
ReaderUI:showReader(book.rootpath .. "/" .. book.lpath)
|
|
|
|
end
|
|
|
|
table.insert(catalog, entry)
|
|
|
|
end
|
|
|
|
if series and not subseries then
|
|
|
|
for index, entry in ipairs(catalog) do
|
|
|
|
catalog[index].text = entry.text:gsub(".00", "", 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return catalog
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
-- find books, series, tags, authors or titles
|
2020-06-19 10:22:38 +00:00
|
|
|
function CalibreSearch:find(option)
|
2023-02-14 23:40:40 +00:00
|
|
|
for _, opt in ipairs(self.default_search_options) do
|
2020-06-19 10:22:38 +00:00
|
|
|
self[opt] = G_reader_settings:nilOrTrue("calibre_search_"..opt)
|
|
|
|
end
|
2023-02-14 23:40:40 +00:00
|
|
|
for _, opt in ipairs(self.extra_search_options) do
|
|
|
|
self[opt] = G_reader_settings:isTrue("calibre_search_"..opt)
|
|
|
|
end
|
2020-06-19 10:22:38 +00:00
|
|
|
|
|
|
|
if #self.libraries == 0 then
|
2021-01-13 10:45:16 +00:00
|
|
|
local libs, err = self.cache_libs:load()
|
2020-06-19 10:22:38 +00:00
|
|
|
if not libs then
|
|
|
|
logger.warn("no calibre libraries", err)
|
|
|
|
self:prompt(_("No calibre libraries"))
|
|
|
|
return
|
|
|
|
else
|
|
|
|
self.libraries = libs
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if #self.books == 0 then
|
|
|
|
self.books = self:getMetadata()
|
|
|
|
end
|
|
|
|
-- this shouldn't happen unless the user disabled all libraries or they are empty.
|
|
|
|
if #self.books == 0 then
|
|
|
|
logger.warn("no metadata to search, aborting")
|
2021-04-02 15:59:29 +00:00
|
|
|
self:prompt(_("No results in metadata"))
|
2020-06-19 10:22:38 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- measure time elapsed searching
|
2022-05-05 19:00:22 +00:00
|
|
|
local start_time = time.now()
|
2022-03-16 20:45:47 +00:00
|
|
|
self:browse(option)
|
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415)
* ReaderDictionary: Port delay computations to TimeVal
* ReaderHighlight: Port delay computations to TimeVal
* ReaderView: Port delay computations to TimeVal
* Android: Reset gesture detection state on APP_CMD_TERM_WINDOW.
This prevents potentially being stuck in bogus gesture states when switching apps.
* GestureDetector:
* Port delay computations to TimeVal
* Fixed delay computations to handle time warps (large and negative deltas).
* Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture.
* Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1.
* Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy.
* The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events.
The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use
a clock source that is *NOT* MONOTONIC.
AFAICT, that's pretty much... PocketBook, and that's it?
* Input:
* Use the <linux/input.h> FFI module instead of re-declaring every constant
* Fixed (verbose) debug logging of input events to actually translate said constants properly.
* Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume.
* Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient.
Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts,
as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector.
* reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary.
* TimeVal:
* Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>).
* Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC.
* Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence.
* New methods:
* Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime
* Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero
* UIManager:
* Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base.
This ensures reliable and consistent scheduling, as time is ensured never to go backwards.
* Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame.
It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value,
because very few time has passed.
The only code left that does live syscalls does it because it's actually necessary for accuracy,
and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics).
* DictQuickLookup: Port delay computations to TimeVal
* FootNoteWidget: Port delay computations to TimeVal
* HTMLBoxWidget: Port delay computations to TimeVal
* Notification: Port delay computations to TimeVal
* TextBoxWidget: Port delay computations to TimeVal
* AutoSuspend: Port to TimeVal
* AutoTurn:
* Fix it so that settings are actually honored.
* Port to TimeVal
* BackgroundRunner: Port to TimeVal
* Calibre: Port benchmarking code to TimeVal
* BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly.
* All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
2021-03-30 00:57:59 +00:00
|
|
|
logger.info(string.format("search done in %.3f milliseconds (%s, %s, %s, %s, %s)",
|
2022-05-05 19:00:22 +00:00
|
|
|
time.to_ms(time.since(start_time)),
|
2020-06-19 10:22:38 +00:00
|
|
|
option == "find" and "books" or option,
|
|
|
|
"case sensitive: " .. tostring(not self.case_insensitive),
|
|
|
|
"title: " .. tostring(self.find_by_title),
|
|
|
|
"authors: " .. tostring(self.find_by_authors),
|
2023-02-14 23:40:40 +00:00
|
|
|
"series: " .. tostring(self.find_by_series),
|
|
|
|
"tag: " .. tostring(self.find_by_tag),
|
2020-06-19 10:22:38 +00:00
|
|
|
"path: " .. tostring(self.find_by_path)))
|
|
|
|
end
|
|
|
|
|
|
|
|
-- find books with current search options
|
2021-01-24 12:47:52 +00:00
|
|
|
function CalibreSearch:findBooks(query)
|
2020-06-19 10:22:38 +00:00
|
|
|
-- handle case sensitivity
|
|
|
|
local function bookMatch(s, p)
|
|
|
|
if not s or not p then return false end
|
|
|
|
if self.case_insensitive then
|
|
|
|
return string.match(string.upper(s), string.upper(p))
|
|
|
|
else
|
|
|
|
return string.match(s, p)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- handle other search preferences
|
|
|
|
local function bookSearch(book, pattern)
|
|
|
|
if self.find_by_title and bookMatch(book.title, pattern) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if self.find_by_authors then
|
|
|
|
for _, author in ipairs(book.authors) do
|
|
|
|
if bookMatch(author, pattern) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2023-02-14 23:40:40 +00:00
|
|
|
if self.find_by_series and bookMatch(book.series, pattern) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
if self.find_by_tag then
|
|
|
|
for _, tag in ipairs(book.tags) do
|
|
|
|
if bookMatch(tag, pattern) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2020-06-19 10:22:38 +00:00
|
|
|
if self.find_by_path and bookMatch(book.lpath, pattern) then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
-- performs a book search
|
|
|
|
local results = {}
|
2021-01-24 12:47:52 +00:00
|
|
|
for i, book in ipairs(self.books) do
|
2020-06-19 10:22:38 +00:00
|
|
|
if bookSearch(book, query) then
|
|
|
|
table.insert(results, #results + 1, book)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
|
|
|
-- browse tags or series
|
2022-03-16 20:45:47 +00:00
|
|
|
function CalibreSearch:browse(option)
|
|
|
|
local search_value
|
|
|
|
if self.search_value ~= "" then
|
|
|
|
search_value = self.search_value
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
2022-03-16 20:45:47 +00:00
|
|
|
local name
|
|
|
|
local menu_entries = {}
|
|
|
|
|
|
|
|
if option == "find" then
|
|
|
|
name = _("Books")
|
|
|
|
menu_entries = self:bookCatalog(self:findBooks(self.search_value))
|
|
|
|
else
|
|
|
|
local source
|
2020-06-19 10:22:38 +00:00
|
|
|
if option == "tags" then
|
|
|
|
name = _("Browse by tags")
|
2023-02-14 23:40:40 +00:00
|
|
|
source = searchByNestedField(self.books, option, search_value, self.case_insensitive)
|
2020-06-19 10:22:38 +00:00
|
|
|
elseif option == "series" then
|
|
|
|
name = _("Browse by series")
|
2023-02-14 23:40:40 +00:00
|
|
|
source = searchByField(self.books, option, search_value, self.case_insensitive)
|
|
|
|
elseif option == "authors" then
|
|
|
|
name = _("Browse by authors")
|
|
|
|
source = searchByNestedField(self.books, option, search_value, self.case_insensitive)
|
|
|
|
elseif option == "title" then
|
|
|
|
name = _("Browse by titles")
|
|
|
|
-- This is admittedly only midly useful in the face of the generic search above,
|
|
|
|
-- but makes finding duplicate titles easy, at least ;).
|
|
|
|
source = searchByField(self.books, option, search_value, self.case_insensitive)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
for k, v in pairs(source) do
|
|
|
|
local entry = {}
|
|
|
|
entry.text = string.format("%s (%d)", k, v)
|
|
|
|
entry.callback = function()
|
2023-02-14 23:40:40 +00:00
|
|
|
self:expandSearchResults(option, k)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
table.insert(menu_entries, entry)
|
|
|
|
end
|
2022-03-16 20:45:47 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
self.search_menu = self.search_menu or Menu:new{
|
|
|
|
width = Screen:getWidth(),
|
|
|
|
height = Screen:getHeight(),
|
|
|
|
parent = nil,
|
|
|
|
is_borderless = true,
|
|
|
|
onMenuHold = self.onMenuHold,
|
|
|
|
}
|
|
|
|
self.search_menu.paths = {}
|
|
|
|
self.search_menu.onReturn = function ()
|
|
|
|
local path_entry = table.remove(self.search_menu.paths)
|
2022-11-27 11:35:18 +00:00
|
|
|
local page = path_entry and path_entry.page or 1
|
2022-03-16 20:45:47 +00:00
|
|
|
if #self.search_menu.paths < 1 then
|
|
|
|
-- If nothing is left in paths we switch to original items and title
|
|
|
|
self.search_menu.paths = {}
|
|
|
|
self:switchResults(menu_entries, name, false, page)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-16 20:45:47 +00:00
|
|
|
self:switchResults(menu_entries, name)
|
|
|
|
UIManager:show(self.search_menu)
|
|
|
|
end
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
function CalibreSearch:expandSearchResults(option, chosen_item)
|
2022-03-16 20:45:47 +00:00
|
|
|
local results
|
|
|
|
|
2023-02-14 23:40:40 +00:00
|
|
|
if option == "tags" or option == "authors" then
|
|
|
|
results = getBooksByNestedField(self.books, option, chosen_item)
|
|
|
|
else
|
|
|
|
results = getBooksByField(self.books, option, chosen_item)
|
2022-03-16 20:45:47 +00:00
|
|
|
end
|
|
|
|
if results then
|
|
|
|
local catalog = self:bookCatalog(results, option)
|
|
|
|
self:switchResults(catalog, chosen_item, true)
|
|
|
|
end
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
|
2022-03-16 20:45:47 +00:00
|
|
|
-- update search results
|
|
|
|
function CalibreSearch:switchResults(t, title, is_child, page)
|
2020-06-19 10:22:38 +00:00
|
|
|
if not title then
|
2021-04-02 15:59:29 +00:00
|
|
|
title = _("Search results")
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
|
2023-01-16 18:36:22 +00:00
|
|
|
local natsort = sort.natsort_cmp(self.natsort_cache)
|
|
|
|
table.sort(t, function(a, b) return natsort(a.text, b.text) end)
|
2022-03-16 20:45:47 +00:00
|
|
|
|
|
|
|
if is_child then
|
|
|
|
local path_entry = {}
|
|
|
|
path_entry.page = (self.search_menu.perpage or 1) * (self.search_menu.page or 1)
|
|
|
|
table.insert(self.search_menu.paths, path_entry)
|
|
|
|
end
|
|
|
|
self.search_menu:switchItemTable(title, t, page or 1)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- prompt the user for a library scan
|
|
|
|
function CalibreSearch:prompt(message)
|
|
|
|
local rootdir = getDefaultRootDir()
|
|
|
|
local warning = T(_("Scanning libraries can take time. All storage media under %1 will be analyzed"), rootdir)
|
|
|
|
if message then
|
|
|
|
message = message .. "\n\n" .. warning
|
|
|
|
end
|
|
|
|
UIManager:show(ConfirmBox:new{
|
|
|
|
text = message or warning,
|
|
|
|
ok_text = _("Scan") .. " " .. rootdir,
|
|
|
|
ok_callback = function()
|
|
|
|
self.libraries = {}
|
2020-12-14 23:46:38 +00:00
|
|
|
local count, paths = self:scan(rootdir)
|
|
|
|
|
2020-06-19 10:22:38 +00:00
|
|
|
-- append current wireless dir if it wasn't found on the scan
|
|
|
|
-- this will happen if it is in a nested dir.
|
|
|
|
local inbox_dir = G_reader_settings:readSetting("inbox_dir")
|
|
|
|
if inbox_dir and not self.libraries[inbox_dir] then
|
|
|
|
if CalibreMetadata:getDeviceInfo(inbox_dir, "date_last_connected") then
|
|
|
|
self.libraries[inbox_dir] = true
|
|
|
|
count = count + 1
|
|
|
|
paths = paths .. "\n" .. count .. ": " .. inbox_dir
|
|
|
|
end
|
|
|
|
end
|
2020-12-14 23:46:38 +00:00
|
|
|
|
|
|
|
-- append libraries in different volumes
|
|
|
|
local ok, sd_path = Device:hasExternalSD()
|
|
|
|
if ok then
|
|
|
|
local sd_count, sd_paths = self:scan(sd_path)
|
|
|
|
count = count + sd_count
|
|
|
|
paths = paths .. "\n" .. _("SD card") .. ": " .. sd_paths
|
|
|
|
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
|
|
|
lfs.mkdir(self.cache_dir)
|
2021-01-13 10:45:16 +00:00
|
|
|
self.cache_libs:save(self.libraries)
|
2020-06-19 10:22:38 +00:00
|
|
|
self:invalidateCache()
|
|
|
|
self.books = self:getMetadata()
|
|
|
|
local info_text
|
|
|
|
if count == 0 then
|
|
|
|
info_text = _("No calibre libraries were found")
|
|
|
|
else
|
2023-03-05 20:09:07 +00:00
|
|
|
info_text = T(_("Found %1 calibre libraries with %2 books:\n%3"), count, #self.books, paths)
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
UIManager:show(InfoMessage:new{ text = info_text })
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2020-12-14 23:46:38 +00:00
|
|
|
function CalibreSearch:scan(rootdir)
|
|
|
|
self.last_scan = {}
|
|
|
|
self:findCalibre(rootdir)
|
|
|
|
local paths = ""
|
|
|
|
for i, dir in ipairs(self.last_scan) do
|
|
|
|
self.libraries[dir.path] = true
|
|
|
|
paths = paths .. "\n" .. i .. ": " .. dir.path
|
|
|
|
end
|
|
|
|
return #self.last_scan, paths
|
|
|
|
end
|
|
|
|
|
2020-06-19 10:22:38 +00:00
|
|
|
-- find all calibre libraries under a given root dir
|
|
|
|
function CalibreSearch:findCalibre(root)
|
|
|
|
-- protect lfs.dir which will raise error on no-permission directory
|
|
|
|
local ok, iter, dir_obj = pcall(lfs.dir, root)
|
|
|
|
local contains_metadata = false
|
|
|
|
if ok then
|
|
|
|
for entity in iter, dir_obj do
|
|
|
|
-- nested libraries aren't allowed
|
|
|
|
if not contains_metadata then
|
|
|
|
if entity ~= "." and entity ~= ".." then
|
|
|
|
local path = root .. "/" .. entity
|
|
|
|
local mode = lfs.attributes(path, "mode")
|
|
|
|
if mode == "file" then
|
|
|
|
if entity == "metadata.calibre" or entity == ".metadata.calibre" then
|
|
|
|
local library = {}
|
|
|
|
library.path = root
|
|
|
|
contains_metadata = true
|
|
|
|
table.insert(self.last_scan, #self.last_scan + 1, library)
|
|
|
|
end
|
|
|
|
elseif mode == "directory" then
|
|
|
|
self:findCalibre(path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- invalidate current cache
|
|
|
|
function CalibreSearch:invalidateCache()
|
2021-01-13 10:45:16 +00:00
|
|
|
self.cache_books:delete()
|
2020-06-19 10:22:38 +00:00
|
|
|
self.books = {}
|
2023-01-16 18:36:22 +00:00
|
|
|
self.natsort_cache = {}
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- get metadata from cache or calibre files
|
|
|
|
function CalibreSearch:getMetadata()
|
2022-05-05 19:00:22 +00:00
|
|
|
local start_time = time.now()
|
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415)
* ReaderDictionary: Port delay computations to TimeVal
* ReaderHighlight: Port delay computations to TimeVal
* ReaderView: Port delay computations to TimeVal
* Android: Reset gesture detection state on APP_CMD_TERM_WINDOW.
This prevents potentially being stuck in bogus gesture states when switching apps.
* GestureDetector:
* Port delay computations to TimeVal
* Fixed delay computations to handle time warps (large and negative deltas).
* Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture.
* Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1.
* Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy.
* The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events.
The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use
a clock source that is *NOT* MONOTONIC.
AFAICT, that's pretty much... PocketBook, and that's it?
* Input:
* Use the <linux/input.h> FFI module instead of re-declaring every constant
* Fixed (verbose) debug logging of input events to actually translate said constants properly.
* Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume.
* Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient.
Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts,
as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector.
* reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary.
* TimeVal:
* Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>).
* Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC.
* Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence.
* New methods:
* Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime
* Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero
* UIManager:
* Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base.
This ensures reliable and consistent scheduling, as time is ensured never to go backwards.
* Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame.
It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value,
because very few time has passed.
The only code left that does live syscalls does it because it's actually necessary for accuracy,
and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics).
* DictQuickLookup: Port delay computations to TimeVal
* FootNoteWidget: Port delay computations to TimeVal
* HTMLBoxWidget: Port delay computations to TimeVal
* Notification: Port delay computations to TimeVal
* TextBoxWidget: Port delay computations to TimeVal
* AutoSuspend: Port to TimeVal
* AutoTurn:
* Fix it so that settings are actually honored.
* Port to TimeVal
* BackgroundRunner: Port to TimeVal
* Calibre: Port benchmarking code to TimeVal
* BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly.
* All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
2021-03-30 00:57:59 +00:00
|
|
|
local template = "metadata: %d books imported from %s in %.3f milliseconds"
|
2020-06-19 10:22:38 +00:00
|
|
|
|
|
|
|
-- try to load metadata from cache
|
|
|
|
if self.cache_metadata then
|
|
|
|
local function cacheIsNewer(timestamp)
|
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 cache_timestamp = self.cache_books:timestamp()
|
|
|
|
-- stat returns a true Epoch (UTC)
|
|
|
|
if not timestamp or not cache_timestamp then return false end
|
2020-06-19 10:22:38 +00:00
|
|
|
local Y, M, D, h, m, s = timestamp:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
|
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
|
|
|
-- calibre also stores this in UTC (c.f., calibre.utils.date.isoformat)...
|
|
|
|
-- But os.time uses mktime, which converts it to *local* time...
|
|
|
|
-- Meaning we'll have to jump through a lot of stupid hoops to make the two agree...
|
|
|
|
local meta_timestamp = os.time({year = Y, month = M, day = D, hour = h, min = m, sec = s})
|
|
|
|
-- To that end, compute the local timezone's offset to UTC via strftime's %z token...
|
|
|
|
local tz = os.date("%z") -- +hhmm or -hhmm
|
|
|
|
-- We deal with a time_t, so, convert that to seconds...
|
|
|
|
local tz_sign, tz_hours, tz_minutes = tz:match("([+-])(%d%d)(%d%d)")
|
|
|
|
local utc_diff = (tonumber(tz_hours) * 60 * 60) + (tonumber(tz_minutes) * 60)
|
|
|
|
if tz_sign == "-" then
|
|
|
|
utc_diff = -utc_diff
|
|
|
|
end
|
|
|
|
meta_timestamp = meta_timestamp + utc_diff
|
|
|
|
logger.dbg("CalibreSearch:getMetadata: Cache timestamp :", cache_timestamp, os.date("!%FT%T.000000+00:00", cache_timestamp), os.date("(%F %T %z)", cache_timestamp))
|
|
|
|
logger.dbg("CalibreSearch:getMetadata: Metadata timestamp:", meta_timestamp, timestamp, os.date("(%F %T %z)", meta_timestamp))
|
|
|
|
|
|
|
|
return cache_timestamp > meta_timestamp
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
2021-01-13 10:45:16 +00:00
|
|
|
|
|
|
|
local cache, err = self.cache_books:load()
|
2020-06-19 10:22:38 +00:00
|
|
|
if not cache then
|
|
|
|
logger.warn("invalid cache:", err)
|
2021-04-13 16:11:39 +00:00
|
|
|
self:invalidateCache()
|
2020-06-19 10:22:38 +00:00
|
|
|
else
|
|
|
|
local is_newer = true
|
|
|
|
for path, enabled in pairs(self.libraries) do
|
|
|
|
if enabled and not cacheIsNewer(CalibreMetadata:getDeviceInfo(path, "date_last_connected")) then
|
|
|
|
is_newer = false
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if is_newer then
|
2022-05-05 19:00:22 +00:00
|
|
|
logger.info(string.format(template, #cache, "cache", time.to_ms(time.since(start_time))))
|
2020-06-19 10:22:38 +00:00
|
|
|
return cache
|
|
|
|
else
|
|
|
|
logger.warn("cache is older than metadata, ignoring it")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- try to load metadata from calibre files and dump it to cache file, if enabled.
|
|
|
|
local books = getAllMetadata(self.libraries)
|
|
|
|
if self.cache_metadata then
|
2021-01-24 12:47:52 +00:00
|
|
|
local serialized_table = {}
|
2020-06-19 10:22:38 +00:00
|
|
|
local function removeNull(t)
|
|
|
|
for _, key in ipairs({"series", "series_index"}) do
|
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 t[key] == rapidjson.null then
|
2020-06-19 10:22:38 +00:00
|
|
|
t[key] = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
for index, book in ipairs(books) do
|
2021-01-24 12:47:52 +00:00
|
|
|
table.insert(serialized_table, index, removeNull(book))
|
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
|
|
|
lfs.mkdir(self.cache_dir)
|
|
|
|
local ok, err = self.cache_books:save(serialized_table)
|
|
|
|
if not ok then
|
|
|
|
logger.info("Failed to serialize calibre metadata cache:", err)
|
|
|
|
end
|
2020-06-19 10:22:38 +00:00
|
|
|
end
|
2022-05-05 19:00:22 +00:00
|
|
|
logger.info(string.format(template, #books, "calibre", time.to_ms(time.since(start_time))))
|
2020-06-19 10:22:38 +00:00
|
|
|
return books
|
|
|
|
end
|
|
|
|
|
|
|
|
return CalibreSearch
|