Calibre: Allow authors/title metadata browse/searches (#10113)

c.f., https://www.mobileread.com/forums/showthread.php?t=352142

This adds dedicated "browse by" buttons for authors & tags, and adds series & tags to the search settings for the meta "Search books" button.
reviewable/pr10128/r1
NiLuJe 1 year ago committed by GitHub
parent 91ff6ce2d8
commit 8e9fc9953f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -30,15 +30,9 @@ function Calibre:onCalibreSearch()
return true return true
end end
function Calibre:onCalibreBrowseTags() function Calibre:onCalibreBrowseBy(field)
CalibreSearch.search_value = "" CalibreSearch.search_value = ""
CalibreSearch:find("tags", 1) CalibreSearch:find(field)
return true
end
function Calibre:onCalibreBrowseSeries()
CalibreSearch.search_value = ""
CalibreSearch:find("series", 1)
return true return true
end end
@ -62,8 +56,10 @@ end
function Calibre:onDispatcherRegisterActions() function Calibre:onDispatcherRegisterActions()
Dispatcher:registerAction("calibre_search", { category="none", event="CalibreSearch", title=_("Calibre metadata search"), general=true,}) Dispatcher:registerAction("calibre_search", { category="none", event="CalibreSearch", title=_("Calibre metadata search"), general=true,})
Dispatcher:registerAction("calibre_browse_tags", { category="none", event="CalibreBrowseTags", title=_("Browse all calibre tags"), general=true,}) Dispatcher:registerAction("calibre_browse_tags", { category="none", event="CalibreBrowseBy", arg="tags", title=_("Browse all calibre tags"), general=true,})
Dispatcher:registerAction("calibre_browse_series", { category="none", event="CalibreBrowseSeries", title=_("Browse all calibre series"), general=true, separator=true,}) Dispatcher:registerAction("calibre_browse_series", { category="none", event="CalibreBrowseBy", arg="series", title=_("Browse all calibre series"), general=true,})
Dispatcher:registerAction("calibre_browse_authors", { category="none", event="CalibreBrowseBy", arg="authors", title=_("Browse all calibre authors"), general=true,})
Dispatcher:registerAction("calibre_browse_titles", { category="none", event="CalibreBrowseBy", arg="title", title=_("Browse all calibre titles"), general=true, separator=true,})
end end
function Calibre:init() function Calibre:init()
@ -209,13 +205,31 @@ function Calibre:getSearchMenuTable()
G_reader_settings:flipNilOrTrue("calibre_search_find_by_authors") G_reader_settings:flipNilOrTrue("calibre_search_find_by_authors")
end, end,
}, },
{
text = _("Search by series"),
checked_func = function()
return G_reader_settings:isTrue("calibre_search_find_by_series")
end,
callback = function()
G_reader_settings:toggle("calibre_search_find_by_series")
end,
},
{
text = _("Search by tag"),
checked_func = function()
return G_reader_settings:isTrue("calibre_search_find_by_tag")
end,
callback = function()
G_reader_settings:toggle("calibre_search_find_by_tag")
end,
},
{ {
text = _("Search by path"), text = _("Search by path"),
checked_func = function() checked_func = function()
return G_reader_settings:nilOrTrue("calibre_search_find_by_path") return G_reader_settings:isTrue("calibre_search_find_by_path")
end, end,
callback = function() callback = function()
G_reader_settings:flipNilOrTrue("calibre_search_find_by_path") G_reader_settings:toggle("calibre_search_find_by_path")
end, end,
}, },
} }

@ -43,7 +43,7 @@ local function slim(book, is_search)
for _, k in ipairs(is_search and search_used_metadata or used_metadata) do for _, k in ipairs(is_search and search_used_metadata or used_metadata) do
if k == "series" or k == "series_index" then if k == "series" or k == "series_index" then
slim_book[k] = book[k] or rapidjson.null slim_book[k] = book[k] or rapidjson.null
elseif k == "tags" then elseif k == "tags" or k == "authors" then
slim_book[k] = book[k] or {} slim_book[k] = book[k] or {}
else else
slim_book[k] = book[k] slim_book[k] = book[k]

@ -67,25 +67,26 @@ local function match(str, query, case_insensitive)
end end
end end
-- get books that exactly match the search tag -- get books that exactly match the search in a specific flat field (series or title)
local function getBooksByTag(t, tag) local function getBooksByField(t, field, query)
local result = {} local result = {}
for _, book in ipairs(t) do for _, book in ipairs(t) do
for __, _tag in ipairs(book.tags) do local data = book[field]
if tag == _tag then -- We can compare nil & rapidjson.null (light userdata) to a string safely
if data == query then
table.insert(result, book) table.insert(result, book)
end end
end end
end
return result return result
end end
-- get books that exactly match the search series -- get books that exactly match the search in a specific array (tags or authors)
local function getBooksBySeries(t, series) local function getBooksByNestedField(t, field, query)
local result = {} local result = {}
for _, book in ipairs(t) do for _, book in ipairs(t) do
if book.series and book.series ~= rapidjson.null then local array = book[field]
if book.series == series then for __, data in ipairs(array) do
if data == query then
table.insert(result, book) table.insert(result, book)
end end
end end
@ -93,28 +94,29 @@ local function getBooksBySeries(t, series)
return result return result
end end
-- get tags that match the search criteria and their frequency -- generic search in a specific flat field (series or title), matching the search criteria and their frequency
local function searchByTag(t, query, case_insensitive) local function searchByField(t, field, query, case_insensitive)
local freq = {} local freq = {}
for _, book in ipairs(t) do for _, book in ipairs(t) do
if type(book.tags) == "table" then local data = book[field]
for __, tag in ipairs(book.tags) do -- We have to make sure we only pass strings to match
if match(tag, query, case_insensitive) then if data and data ~= rapidjson.null then
freq[tag] = (freq[tag] or 0) + 1 if match(data, query, case_insensitive) then
end freq[data] = (freq[data] or 0) + 1
end end
end end
end end
return freq return freq
end end
-- get series that match the search criteria and their frequency -- generic search in a specific array (tags or authors), matching the search criteria and their frequency
local function searchBySeries(t, query, case_insensitive) local function searchByNestedField(t, field, query, case_insensitive)
local freq = {} local freq = {}
for _, book in ipairs(t) do for _, book in ipairs(t) do
if book.series and book.series ~= rapidjson.null then local array = book[field]
if match(book.series, query, case_insensitive) then for __, data in ipairs(array) do
freq[book.series] = (freq[book.series] or 0) + 1 if match(data, query, case_insensitive) then
freq[data] = (freq[data] or 0) + 1
end end
end end
end end
@ -162,11 +164,17 @@ local CalibreSearch = WidgetContainer:extend{
libraries = {}, libraries = {},
natsort_cache = {}, natsort_cache = {},
last_scan = {}, last_scan = {},
search_options = { -- These are enabled by default
default_search_options = {
"cache_metadata", "cache_metadata",
"case_insensitive", "case_insensitive",
"find_by_title", "find_by_title",
"find_by_authors", "find_by_authors",
},
-- These aren't
extra_search_options = {
"find_by_series",
"find_by_tag",
"find_by_path", "find_by_path",
}, },
@ -205,6 +213,26 @@ function CalibreSearch:ShowSearch()
end, end,
}, },
}, },
{
{
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,
},
},
{ {
{ {
text = _("Cancel"), text = _("Cancel"),
@ -300,11 +328,14 @@ function CalibreSearch:bookCatalog(t, option)
return catalog return catalog
end end
-- find books, series or tags -- find books, series, tags, authors or titles
function CalibreSearch:find(option) function CalibreSearch:find(option)
for _, opt in ipairs(self.search_options) do for _, opt in ipairs(self.default_search_options) do
self[opt] = G_reader_settings:nilOrTrue("calibre_search_"..opt) self[opt] = G_reader_settings:nilOrTrue("calibre_search_"..opt)
end end
for _, opt in ipairs(self.extra_search_options) do
self[opt] = G_reader_settings:isTrue("calibre_search_"..opt)
end
if #self.libraries == 0 then if #self.libraries == 0 then
local libs, err = self.cache_libs:load() local libs, err = self.cache_libs:load()
@ -336,6 +367,8 @@ function CalibreSearch:find(option)
"case sensitive: " .. tostring(not self.case_insensitive), "case sensitive: " .. tostring(not self.case_insensitive),
"title: " .. tostring(self.find_by_title), "title: " .. tostring(self.find_by_title),
"authors: " .. tostring(self.find_by_authors), "authors: " .. tostring(self.find_by_authors),
"series: " .. tostring(self.find_by_series),
"tag: " .. tostring(self.find_by_tag),
"path: " .. tostring(self.find_by_path))) "path: " .. tostring(self.find_by_path)))
end end
@ -362,6 +395,16 @@ function CalibreSearch:findBooks(query)
end end
end end
end end
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
if self.find_by_path and bookMatch(book.lpath, pattern) then if self.find_by_path and bookMatch(book.lpath, pattern) then
return true return true
end end
@ -393,16 +436,24 @@ function CalibreSearch:browse(option)
local source local source
if option == "tags" then if option == "tags" then
name = _("Browse by tags") name = _("Browse by tags")
source = searchByTag(self.books, search_value, self.case_insensitive) source = searchByNestedField(self.books, option, search_value, self.case_insensitive)
elseif option == "series" then elseif option == "series" then
name = _("Browse by series") name = _("Browse by series")
source = searchBySeries(self.books, search_value, self.case_insensitive) 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)
end end
for k, v in pairs(source) do for k, v in pairs(source) do
local entry = {} local entry = {}
entry.text = string.format("%s (%d)", k, v) entry.text = string.format("%s (%d)", k, v)
entry.callback = function() entry.callback = function()
self:expandTagOrSeries(option,k) self:expandSearchResults(option, k)
end end
table.insert(menu_entries, entry) table.insert(menu_entries, entry)
end end
@ -430,13 +481,13 @@ function CalibreSearch:browse(option)
UIManager:show(self.search_menu) UIManager:show(self.search_menu)
end end
function CalibreSearch:expandTagOrSeries(option, chosen_item) function CalibreSearch:expandSearchResults(option, chosen_item)
local results local results
if option == "tags" then if option == "tags" or option == "authors" then
results = getBooksByTag(self.books, chosen_item) results = getBooksByNestedField(self.books, option, chosen_item)
elseif option == "series" then else
results = getBooksBySeries(self.books, chosen_item) results = getBooksByField(self.books, option, chosen_item)
end end
if results then if results then
local catalog = self:bookCatalog(results, option) local catalog = self:bookCatalog(results, option)

Loading…
Cancel
Save