Custom metadata (#10861)

reviewable/pr10866/r1
hius07 8 months ago committed by GitHub
parent 0ef7729678
commit ed2ea6803f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,18 +4,20 @@ This module provides a way to display book information (filename and book metada
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DocSettings = require("docsettings")
local Document = require("document/document")
local DocumentRegistry = require("document/documentregistry")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local TextViewer = require("ui/widget/textviewer")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local ffiutil = require("ffi/util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local BookInfo = WidgetContainer:extend{
props = {
@ -27,6 +29,17 @@ local BookInfo = WidgetContainer:extend{
"keywords",
"description",
},
prop_text = {
cover = _("Cover image:"),
title = _("Title:"),
authors = _("Authors:"),
series = _("Series:"),
series_index = _("Series index:"),
language = _("Language:"),
keywords = _("Keywords:"),
description = _("Description:"),
pages = _("Pages:"),
},
}
function BookInfo:init()
@ -46,7 +59,7 @@ end
-- Shows book information.
function BookInfo:show(file, book_props, metadata_updated_caller_callback)
self.updated = nil
self.prop_updated = nil
local kv_pairs = {}
-- File section
@ -66,19 +79,31 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
-- book_props may be provided if caller already has them available
-- but it may lack "pages", that we may get from sidecar file
if not book_props or not book_props.pages then
book_props = BookInfo.getDocProps(nil, file, book_props)
book_props = BookInfo.getDocProps(file, book_props)
end
-- cover image
self.custom_book_cover = DocSettings:findCoverFile(file)
local key_text = self.prop_text["cover"]
if self.custom_book_cover then
key_text = "\u{F040} " .. key_text
end
table.insert(kv_pairs, { key_text, _("Tap to display"),
callback = function()
self:onShowBookCover(file)
end,
hold_callback = function()
self:showCustomDialog(file, book_props, metadata_updated_caller_callback)
end,
separator = true,
})
-- metadata
local custom_props
local custom_metadata_file = DocSettings:getCustomMetadataFile(file)
if custom_metadata_file then
self.custom_doc_settings = DocSettings:openCustomMetadata(custom_metadata_file)
custom_props = self.custom_doc_settings:readSetting("custom_props")
end
local values_lang
local prop_text = {
title = _("Title:"),
authors = _("Authors:"),
series = _("Series:"),
series_index = _("Series index:"),
pages = _("Pages:"), -- not in document metadata
language = _("Language:"),
keywords = _("Keywords:"),
description = _("Description:"),
}
for _i, prop_key in ipairs(self.props) do
local prop = book_props[prop_key]
if prop == nil or prop == "" then
@ -103,33 +128,23 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
-- Description may (often in EPUB, but not always) or may not (rarely in PDF) be HTML
prop = util.htmlToPlainTextIfHtml(prop)
end
table.insert(kv_pairs, { prop_text[prop_key], prop })
if prop_key == "series_index" then
table.insert(kv_pairs, { prop_text["pages"], book_props["pages"] or _("N/A") })
key_text = self.prop_text[prop_key]
if custom_props and custom_props[prop_key] then -- customized
key_text = "\u{F040} " .. key_text
end
end
-- cover image
local is_doc = self.document and true or false
self.custom_book_cover = DocSettings:findCoverFile(file)
table.insert(kv_pairs, {
_("Cover image:"),
_("Tap to display"),
callback = function() self:onShowBookCover(file, true) end,
separator = is_doc and not self.custom_book_cover,
})
-- custom cover image
if self.custom_book_cover then
table.insert(kv_pairs, {
_("Custom cover image:"),
_("Tap to display"),
callback = function() self:onShowBookCover(file) end,
separator = is_doc,
table.insert(kv_pairs, { key_text, prop,
hold_callback = function()
self:showCustomDialog(file, book_props, metadata_updated_caller_callback, prop_key)
end,
})
end
-- pages
local is_doc = self.document and true or false
table.insert(kv_pairs, { self.prop_text["pages"], book_props["pages"] or _("N/A"), separator = is_doc })
-- Page section
if is_doc then
local lines_nb, words_nb = self:getCurrentPageLineWordCounts()
local lines_nb, words_nb = self.ui.view:getCurrentPageLineWordCounts()
if lines_nb == 0 then
lines_nb = _("N/A")
words_nb = _("N/A")
@ -145,16 +160,19 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
kv_pairs = kv_pairs,
values_lang = values_lang,
close_callback = function()
self.custom_doc_settings = nil
self.custom_book_cover = nil
if self.updated then
local FileManager = require("apps/filemanager/filemanager")
local fm_ui = FileManager.instance
local ui = self.ui or fm_ui
if not ui then
local ReaderUI = require("apps/reader/readerui")
ui = ReaderUI.instance
if self.prop_updated then
local ui, fm_ui
if self.ui then
if self.prop_updated == "title" then
self.ui.view.footer:updateFooterText() -- in case the title changed
end
else
fm_ui = require("apps/filemanager/filemanager").instance
end
if ui and ui.coverbrowser then -- refresh cache db
ui = self.ui or fm_ui
if ui.coverbrowser then -- refresh cache db
ui.coverbrowser:deleteBookInfo(file)
end
if fm_ui then
@ -165,21 +183,19 @@ function BookInfo:show(file, book_props, metadata_updated_caller_callback)
end
end
end,
title_bar_left_icon = "appbar.menu",
title_bar_left_icon_tap_callback = function()
self:showCustomMenu(file, book_props, metadata_updated_caller_callback)
end,
}
UIManager:show(self.kvp_widget)
end
-- Returns customized metadata.
function BookInfo.customizeProps(original_props, filepath)
local custom_props = {} -- stub
-- Returns extended and customized metadata.
function BookInfo.extendProps(original_props, filepath)
local custom_metadata_file = DocSettings:getCustomMetadataFile(filepath)
local custom_props = custom_metadata_file
and DocSettings:openCustomMetadata(custom_metadata_file):readSetting("custom_props") or {}
original_props = original_props or {}
local props = {}
for _i, prop_key in ipairs(BookInfo.props) do
for _, prop_key in ipairs(BookInfo.props) do
props[prop_key] = custom_props[prop_key] or original_props[prop_key]
end
props.pages = original_props.pages
@ -188,21 +204,8 @@ function BookInfo.customizeProps(original_props, filepath)
return props
end
-- Returns document metadata (opened document or book (file) metadata or custom metadata).
function BookInfo.getDocProps(ui, file, book_props, no_open_document, no_customize)
local original_props, filepath
if ui then -- currently opened document
original_props = ui.doc_settings:readSetting("doc_props")
filepath = ui.document.file
else -- from file
original_props = BookInfo.getBookProps(file, book_props, no_open_document)
filepath = file
end
return no_customize and original_props or BookInfo.customizeProps(original_props, filepath)
end
-- Returns book (file) metadata, including number of pages.
function BookInfo.getBookProps(file, book_props, no_open_document)
-- Returns customized document metadata, including number of pages.
function BookInfo.getDocProps(file, book_props, no_open_document)
if DocSettings:hasSidecarFile(file) then
local doc_settings = DocSettings:open(file)
if not book_props then
@ -228,7 +231,16 @@ function BookInfo.getBookProps(file, book_props, no_open_document)
end
end
-- If still no book_props (book never opened or empty "stats"), open the document to get them
-- If still no book_props (book never opened or empty "stats"),
-- but custom metadata exists, it has a copy of original doc_props
if not book_props then
local custom_metadata_file = DocSettings:getCustomMetadataFile(file)
if custom_metadata_file then
book_props = DocSettings:openCustomMetadata(custom_metadata_file):readSetting("doc_props")
end
end
-- If still no book_props, open the document to get them
if not book_props and not no_open_document then
local document = DocumentRegistry:openDocument(file)
if document then
@ -257,8 +269,7 @@ function BookInfo.getBookProps(file, book_props, no_open_document)
end
end
-- If still no book_props, fall back to empty ones
return book_props or {}
return BookInfo.extendProps(book_props, file)
end
-- Shows book information for currently opened document.
@ -269,23 +280,26 @@ function BookInfo:onShowBookInfo()
end
end
function BookInfo:showBookProp(prop_key, prop_text)
if prop_key == "description" then
prop_text = util.htmlToPlainTextIfHtml(prop_text)
end
UIManager:show(TextViewer:new{
title = self.prop_text[prop_key],
text = prop_text,
})
end
function BookInfo:onShowBookDescription(description, file)
if not description then
if file then
description = BookInfo.getDocProps(nil, file).description
description = BookInfo.getDocProps(file).description
elseif self.document then -- currently opened document
description = self.ui.doc_props.description
end
end
if description and description ~= "" then
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(description)
local TextViewer = require("ui/widget/textviewer")
UIManager:show(TextViewer:new{
title = _("Description:"),
text = description,
})
if description then
self:showBookProp("description", description)
else
UIManager:show(InfoMessage:new{
text = _("No book description available."),
@ -341,28 +355,21 @@ function BookInfo:getCoverImage(doc, file, force_orig)
return cover_bb
end
function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
local function kvp_update()
if self.ui then
self.ui.doc_settings:getCoverFile(true) -- reset cover file cache
end
self.updated = true
self.kvp_widget:onClose()
self:show(file, book_props, metadata_updated_caller_callback)
function BookInfo:updateBookInfo(file, book_props, metadata_updated_caller_callback, prop_updated)
if prop_updated == "cover" and self.ui then
self.ui.doc_settings:getCoverFile(true) -- reset cover file cache
end
self.prop_updated = prop_updated
self.kvp_widget:onClose()
self:show(file, book_props, metadata_updated_caller_callback)
end
function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
if self.custom_book_cover then -- reset custom cover
local ConfirmBox = require("ui/widget/confirmbox")
local confirm_box = ConfirmBox:new{
text = _("Reset custom cover?\nImage file will be deleted."),
ok_text = _("Reset"),
ok_callback = function()
if os.remove(self.custom_book_cover) then
DocSettings:removeSidecarDir(file, util.splitFilePathName(self.custom_book_cover))
kvp_update()
end
end,
}
UIManager:show(confirm_box)
if os.remove(self.custom_book_cover) then
DocSettings:removeSidecarDir(file, util.splitFilePathName(self.custom_book_cover))
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, "cover")
end
else -- choose an image and set custom cover
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
@ -371,22 +378,8 @@ function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_c
return DocumentRegistry:isImageFile(filename)
end,
onConfirm = function(image_file)
local sidecar_dir
local sidecar_file = DocSettings:findCoverFile(file) -- existing cover file
if sidecar_file then
os.remove(sidecar_file)
else -- no existing cover, get metadata file path
sidecar_file = DocSettings:hasSidecarFile(file, true) -- new sdr locations only
end
if sidecar_file then
sidecar_dir = util.splitFilePathName(sidecar_file)
else -- no sdr folder, create new
sidecar_dir = DocSettings:getSidecarDir(file) .. "/"
util.makePath(sidecar_dir)
end
local new_cover_file = sidecar_dir .. "cover." .. util.getFileNameSuffix(image_file):lower()
if ffiutil.copyFile(image_file, new_cover_file) == nil then
kvp_update()
if DocSettings:flushCustomCover(file, image_file) then
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, "cover")
end
end,
}
@ -394,61 +387,153 @@ function BookInfo:setCustomBookCover(file, book_props, metadata_updated_caller_c
end
end
function BookInfo:getCurrentPageLineWordCounts()
local lines_nb, words_nb = 0, 0
if self.ui.rolling then
local res = self.ui.document:getTextFromPositions({x = 0, y = 0},
{x = Screen:getWidth(), y = Screen:getHeight()}, true) -- do not highlight
if res then
lines_nb = #self.ui.document:getScreenBoxesFromPositions(res.pos0, res.pos1, true)
for word in util.gsplit(res.text, "[%s%p]+", false) do
if util.hasCJKChar(word) then
for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do
words_nb = words_nb + 1
end
else
words_nb = words_nb + 1
end
end
end
function BookInfo:setCustomMetadata(file, book_props, metadata_updated_caller_callback, prop_key, prop_value)
-- in file
local custom_doc_settings, custom_props, display_title
if self.custom_doc_settings then
custom_doc_settings = self.custom_doc_settings
custom_props = custom_doc_settings:readSetting("custom_props")
else -- no custom metadata file, create new
custom_doc_settings = DocSettings:openCustomMetadata()
custom_props = {}
display_title = book_props.display_title -- backup
book_props.display_title = nil
custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props
end
custom_props[prop_key] = prop_value -- nil when resetting a custom prop
if next(custom_props) == nil then -- no more custom metadata
os.remove(custom_doc_settings.custom_metadata_file)
DocSettings:removeSidecarDir(file, util.splitFilePathName(custom_doc_settings.custom_metadata_file))
else
local page_boxes = self.ui.document:getTextBoxes(self.ui:getCurrentPage())
if page_boxes and page_boxes[1][1].word then
lines_nb = #page_boxes
for _, line in ipairs(page_boxes) do
if #line == 1 and line[1].word == "" then -- empty line
lines_nb = lines_nb - 1
else
words_nb = words_nb + #line
local last_word = line[#line].word
if last_word:sub(-1) == "-" and last_word ~= "-" then -- hyphenated
words_nb = words_nb - 1
end
end
end
custom_doc_settings:saveSetting("custom_props", custom_props)
custom_doc_settings:flushCustomMetadata(file)
end
book_props.display_title = book_props.display_title or display_title -- restore
-- in memory
prop_value = prop_value or custom_doc_settings:readSetting("doc_props")[prop_key] -- set custom or restore original
book_props[prop_key] = prop_value
if self.ui then -- currently opened document
self.ui.doc_props[prop_key] = prop_value
if prop_key == "title" then -- generate if original is empty
self.ui.doc_props.display_title = prop_value or filemanagerutil.splitFileNameType(file)
end
end
return lines_nb, words_nb
self:updateBookInfo(file, book_props, metadata_updated_caller_callback, prop_key)
end
function BookInfo:showCustomMenu(file, book_props, metadata_updated_caller_callback)
function BookInfo:showCustomEditDialog(file, book_props, metadata_updated_caller_callback, prop_key)
local input_dialog
input_dialog = InputDialog:new{
title = _("Edit book property:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
input = book_props[prop_key],
input_type = prop_key == "series_index" and "number",
allow_newline = prop_key == "authors" or prop_key == "keywords" or prop_key == "description",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
callback = function()
local prop_value = input_dialog:getInputValue()
if prop_value and prop_value ~= "" then
UIManager:close(input_dialog)
self:setCustomMetadata(file, book_props, metadata_updated_caller_callback, prop_key, prop_value)
end
end,
},
},
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function BookInfo:showCustomDialog(file, book_props, metadata_updated_caller_callback, prop_key)
local original_prop, custom_prop, prop_is_cover
if prop_key then -- metadata
if self.custom_doc_settings then
original_prop = self.custom_doc_settings:readSetting("doc_props")[prop_key]
custom_prop = self.custom_doc_settings:readSetting("custom_props")[prop_key]
else
original_prop = book_props[prop_key]
end
if original_prop and prop_key == "description" then
original_prop = util.htmlToPlainTextIfHtml(original_prop)
end
prop_is_cover = false
else -- cover
prop_key = "cover"
prop_is_cover = true
end
local button_dialog
local buttons = {{
local buttons = {
{
text = self.custom_book_cover and _("Reset cover image") or _("Set cover image"),
align = "left",
callback = function()
UIManager:close(button_dialog)
self:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
end,
{
text = _("Copy original"),
enabled = original_prop ~= nil and Device:hasClipboard(),
callback = function()
UIManager:close(button_dialog)
Device.input.setClipboardText(original_prop)
end,
},
{
text = _("View original"),
enabled = original_prop ~= nil or prop_is_cover,
callback = function()
if prop_is_cover then
self:onShowBookCover(file, true)
else
self:showBookProp(prop_key, original_prop)
end
end,
},
},
}}
{
{
text = _("Reset custom"),
enabled = custom_prop ~= nil or (prop_is_cover and self.custom_book_cover ~= nil),
callback = function()
local confirm_box = ConfirmBox:new{
text = prop_is_cover and _("Reset custom cover?\nImage file will be deleted.")
or _("Reset custom book property?"),
ok_text = _("Reset"),
ok_callback = function()
UIManager:close(button_dialog)
if prop_is_cover then
self:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
else
self:setCustomMetadata(file, book_props, metadata_updated_caller_callback, prop_key)
end
end,
}
UIManager:show(confirm_box)
end,
},
{
text = _("Set custom"),
enabled = not prop_is_cover or (prop_is_cover and self.custom_book_cover == nil),
callback = function()
UIManager:close(button_dialog)
if prop_is_cover then
self:setCustomBookCover(file, book_props, metadata_updated_caller_callback)
else
self:showCustomEditDialog(file, book_props, metadata_updated_caller_callback, prop_key)
end
end,
},
},
}
button_dialog = ButtonDialog:new{
shrink_unneeded_width = true,
title = _("Book property:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
title_align = "center",
buttons = buttons,
anchor = function()
return self.kvp_widget.title_bar.left_button.image.dimen
end,
}
UIManager:show(button_dialog)
end

@ -193,7 +193,7 @@ function FileSearcher:isFileMatch(filename, fullpath, keywords, is_file)
end
if self.include_metadata and is_file and DocumentRegistry:hasProvider(fullpath) then
local book_props = self.ui.coverbrowser:getBookInfo(fullpath) or
FileManagerBookInfo.getDocProps(nil, fullpath, nil, true)
FileManagerBookInfo.getDocProps(fullpath, nil, true) -- do not open the document
if next(book_props) ~= nil then
for _, key in ipairs(FileManagerBookInfo.props) do
local prop = book_props[key]

@ -778,7 +778,7 @@ function ReaderTypography:onPreRenderDocument(config)
-- This is called after the document has been loaded,
-- when we know and can access the document language.
local props = self.ui.document:getProps()
local doc_language = FileManagerBookInfo.customizeProps(props, self.ui.document.file).language
local doc_language = FileManagerBookInfo.extendProps(props, self.ui.document.file).language
self.book_lang_tag = self:fixLangTag(doc_language)
local is_known_lang_tag = self.book_lang_tag and LANG_TAG_TO_LANG_NAME[self.book_lang_tag] ~= nil

@ -22,6 +22,7 @@ local logger = require("logger")
local optionsutil = require("ui/data/optionsutil")
local Size = require("ui/size")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template
@ -636,7 +637,7 @@ function ReaderView:drawHighlightRect(bb, _x, _y, rect, drawer, draw_note_mark)
else
local note_mark_pos_x
if self.ui.paging or
(self.ui.document:getVisiblePageCount() == 1) or -- one-page mode
(self.document:getVisiblePageCount() == 1) or -- one-page mode
(x < Screen:getWidth() / 2) then -- page 1 in two-page mode
note_mark_pos_x = self.note_mark_pos_x1
else
@ -1247,17 +1248,17 @@ function ReaderView:setupNoteMarkPosition()
self.note_mark_pos_x1 = screen_w - sign_gap - sign_w
end
else
local doc_margins = self.ui.document:getPageMargins()
local doc_margins = self.document:getPageMargins()
local pos_x_r = screen_w - doc_margins["right"] + sign_gap -- mark in the right margin
local pos_x_l = doc_margins["left"] - sign_gap - sign_w -- mark in the left margin
if self.ui.document:getVisiblePageCount() == 1 then
if self.document:getVisiblePageCount() == 1 then
if BD.mirroredUILayout() then
self.note_mark_pos_x1 = pos_x_l
else
self.note_mark_pos_x1 = pos_x_r
end
else -- two-page mode
local page2_x = self.ui.document:getPageOffsetX(self.ui.document:getCurrentPage(true)+1)
local page2_x = self.document:getPageOffsetX(self.document:getCurrentPage(true)+1)
if BD.mirroredUILayout() then
self.note_mark_pos_x1 = pos_x_l
self.note_mark_pos_x2 = pos_x_l + page2_x
@ -1270,4 +1271,41 @@ function ReaderView:setupNoteMarkPosition()
end
end
function ReaderView:getCurrentPageLineWordCounts()
local lines_nb, words_nb = 0, 0
if self.ui.rolling then
local res = self.document:getTextFromPositions({x = 0, y = 0},
{x = Screen:getWidth(), y = Screen:getHeight()}, true) -- do not highlight
if res then
lines_nb = #self.document:getScreenBoxesFromPositions(res.pos0, res.pos1, true)
for word in util.gsplit(res.text, "[%s%p]+", false) do
if util.hasCJKChar(word) then
for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do
words_nb = words_nb + 1
end
else
words_nb = words_nb + 1
end
end
end
else
local page_boxes = self.document:getTextBoxes(self.ui:getCurrentPage())
if page_boxes and page_boxes[1][1].word then
lines_nb = #page_boxes
for _, line in ipairs(page_boxes) do
if #line == 1 and line[1].word == "" then -- empty line
lines_nb = lines_nb - 1
else
words_nb = words_nb + #line
local last_word = line[#line].word
if last_word:sub(-1) == "-" and last_word ~= "-" then -- hyphenated
words_nb = words_nb - 1
end
end
end
end
end
return lines_nb, words_nb
end
return ReaderView

@ -456,9 +456,10 @@ function ReaderUI:init()
-- Now that document is loaded, store book metadata in settings
-- (so that filemanager can use it from sideCar file to display
-- Book information).
self.doc_settings:saveSetting("doc_props", self.document:getProps())
local props = self.document:getProps()
self.doc_settings:saveSetting("doc_props", props)
-- And have an extended and customized copy in memory for quick access.
self.doc_props = FileManagerBookInfo.getDocProps(self)
self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file)
-- Set "reading" status if there is no status.
local summary = self.doc_settings:readSetting("summary")

@ -16,6 +16,7 @@ local DocSettings = LuaSettings:extend{}
local HISTORY_DIR = DataStorage:getHistoryDir()
local DOCSETTINGS_DIR = DataStorage:getDocSettingsDir()
local custom_metadata_filename = "custom_metadata.lua"
local function buildCandidates(list)
local candidates = {}
@ -146,41 +147,6 @@ function DocSettings:getFileFromHistory(hist_name)
end
end
--- Returns path to book custom cover file if it exists, or nil.
function DocSettings:findCoverFile(doc_path)
local location = G_reader_settings:readSetting("document_metadata_folder", "doc")
local sidecar_dir = self:getSidecarDir(doc_path, location)
local cover_file = self:_findCoverFileInDir(sidecar_dir)
if not cover_file then
location = location == "doc" and "dir" or "doc"
sidecar_dir = self:getSidecarDir(doc_path, location)
cover_file = self:_findCoverFileInDir(sidecar_dir)
end
return cover_file
end
function DocSettings:_findCoverFileInDir(dir)
local ok, iter, dir_obj = pcall(lfs.dir, dir)
if ok then
for f in iter, dir_obj do
if util.splitFileNameSuffix(f) == "cover" then
return dir .. "/" .. f
end
end
end
end
function DocSettings:getCoverFile(reset_cache)
if reset_cache then
self.cover_file = nil
else
if self.cover_file == nil then
self.cover_file = DocSettings:findCoverFile(self.data.doc_path) or false
end
return self.cover_file
end
end
--- Opens a document's individual settings (font, margin, dictionary, etc.)
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`)
-- @treturn DocSettings object
@ -252,8 +218,16 @@ function DocSettings:open(doc_path)
return new
end
function DocSettings.writeFile(f_out, s_out)
f_out:write("-- we can read Lua syntax here!\nreturn ")
f_out:write(s_out)
f_out:write("\n")
ffiutil.fsyncOpenedFile(f_out) -- force flush to the storage device
f_out:close()
end
--- Serializes settings and writes them to `metadata.lua`.
function DocSettings:flush(data, no_cover)
function DocSettings:flush(data, no_custom_metadata)
-- Depending on the settings, doc_settings are saved to the book folder or
-- to koreader/docsettings folder. The latter is also a fallback for read-only book storage.
local serials = G_reader_settings:readSetting("document_metadata_folder", "doc") == "doc"
@ -281,28 +255,35 @@ function DocSettings:flush(data, no_cover)
logger.dbg("DocSettings: Writing to", sidecar_file)
local f_out = io.open(sidecar_file, "w")
if f_out ~= nil then
f_out:write("-- we can read Lua syntax here!\nreturn ")
f_out:write(s_out)
f_out:write("\n")
ffiutil.fsyncOpenedFile(f_out) -- force flush to the storage device
f_out:close()
DocSettings.writeFile(f_out, s_out)
if directory_updated then
-- Ensure the file renaming is flushed to storage device
ffiutil.fsyncDirectory(sidecar_file)
end
-- move cover file to the metadata file location
if not no_cover then
local cover_file = self:getCoverFile()
if cover_file then
local filepath, filename = util.splitFilePathName(cover_file)
-- move custom cover file and custom metadata file to the metadata file location
if not no_custom_metadata then
local metadata_file, filepath, filename
-- custom cover
metadata_file = self:getCoverFile()
if metadata_file then
filepath, filename = util.splitFilePathName(metadata_file)
if filepath ~= sidecar_dir .. "/" then
ffiutil.copyFile(cover_file, sidecar_dir .. "/" .. filename)
os.remove(cover_file)
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename)
os.remove(metadata_file)
self:getCoverFile(true) -- reset cache
end
end
-- custom metadata
metadata_file = self:getCustomMetadataFile()
if metadata_file then
filepath, filename = util.splitFilePathName(metadata_file)
if filepath ~= sidecar_dir .. "/" then
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename)
os.remove(metadata_file)
end
end
end
self:purge(sidecar_file) -- remove old candidates and empty sidecar folders
@ -330,13 +311,21 @@ function DocSettings:purge(sidecar_to_keep)
local custom_metadata_purged
if not sidecar_to_keep then
local cover_file = self:getCoverFile()
if cover_file then
os.remove(cover_file)
-- custom cover
local metadata_file = self:getCoverFile()
if metadata_file then
os.remove(metadata_file)
self:getCoverFile(true) -- reset cache
custom_metadata_purged = true
end
-- custom metadata
metadata_file = self:getCustomMetadataFile()
if metadata_file then
os.remove(metadata_file)
custom_metadata_purged = true
end
end
if lfs.attributes(self.doc_sidecar_dir, "mode") == "directory" then
os.remove(self.doc_sidecar_dir) -- keep parent folders
end
@ -361,11 +350,11 @@ function DocSettings:updateLocation(doc_path, new_doc_path, copy)
local doc_settings, new_sidecar_dir
-- update metadata
if self:hasSidecarFile(doc_path) then
if DocSettings:hasSidecarFile(doc_path) then
doc_settings = DocSettings:open(doc_path)
if new_doc_path then
local new_doc_settings = DocSettings:open(new_doc_path)
-- save doc settings to the new location, no cover file yet
-- save doc settings to the new location, no custom metadata yet
new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true)
else
local cache_file_path = doc_settings:readSetting("cache_file_path")
@ -375,26 +364,155 @@ function DocSettings:updateLocation(doc_path, new_doc_path, copy)
end
end
-- update cover file
-- update custom metadata
if not doc_settings then
doc_settings = DocSettings:open(doc_path)
end
local cover_file = doc_settings:getCoverFile()
if cover_file and new_doc_path then
if not new_sidecar_dir then
new_sidecar_dir = self:getSidecarDir(new_doc_path)
util.makePath(new_sidecar_dir)
if new_doc_path then
-- custom cover
if cover_file then
if not new_sidecar_dir then
new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path)
util.makePath(new_sidecar_dir)
end
local _, filename = util.splitFilePathName(cover_file)
ffiutil.copyFile(cover_file, new_sidecar_dir .. "/" .. filename)
end
-- custom metadata
local metadata_file = self:getCustomMetadataFile(doc_path)
if metadata_file then
if not new_sidecar_dir then
new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path)
util.makePath(new_sidecar_dir)
end
ffiutil.copyFile(metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename)
end
local _, filename = util.splitFilePathName(cover_file)
ffiutil.copyFile(cover_file, new_sidecar_dir .. "/" .. filename)
end
if not copy then
doc_settings:purge()
end
if cover_file then
if cover_file then -- after purge because purge uses cover file cache
doc_settings:getCoverFile(true) -- reset cache
end
end
-- custom cover
--- Returns path to book custom cover file if it exists, or nil.
function DocSettings:findCoverFile(doc_path)
doc_path = doc_path or self.data.doc_path
local location = G_reader_settings:readSetting("document_metadata_folder", "doc")
local sidecar_dir = self:getSidecarDir(doc_path, location)
local cover_file = DocSettings._findCoverFileInDir(sidecar_dir)
if not cover_file then
location = location == "doc" and "dir" or "doc"
sidecar_dir = self:getSidecarDir(doc_path, location)
cover_file = DocSettings._findCoverFileInDir(sidecar_dir)
end
return cover_file
end
function DocSettings._findCoverFileInDir(dir)
local ok, iter, dir_obj = pcall(lfs.dir, dir)
if ok then
for f in iter, dir_obj do
if util.splitFileNameSuffix(f) == "cover" then
return dir .. "/" .. f
end
end
end
end
function DocSettings:getCoverFile(reset_cache)
if reset_cache then
self.cover_file = nil
else
if self.cover_file == nil then -- fill empty cache
self.cover_file = self:findCoverFile() or false
end
return self.cover_file
end
end
function DocSettings:getCustomCandidateSidecarDirs(doc_path)
local sidecar_file = self:hasSidecarFile(doc_path, true) -- new locations only
if sidecar_file then -- book was opened, write custom metadata to its sidecar dir
local sidecar_dir = util.splitFilePathName(sidecar_file):sub(1, -2)
return { sidecar_dir }
end
-- new book, create sidecar dir in accordance with sdr location setting
local dir_sidecar_dir = self:getSidecarDir(doc_path, "dir")
if G_reader_settings:readSetting("document_metadata_folder", "doc") == "doc" then
local doc_sidecar_dir = self:getSidecarDir(doc_path, "doc")
return { doc_sidecar_dir, dir_sidecar_dir } -- fallback in case of readonly book storage
end
return { dir_sidecar_dir }
end
function DocSettings:flushCustomCover(doc_path, image_file)
local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path)
local new_cover_filename = "/cover." .. util.getFileNameSuffix(image_file):lower()
for _, sidecar_dir in ipairs(sidecar_dirs) do
util.makePath(sidecar_dir)
local new_cover_file = sidecar_dir .. new_cover_filename
if ffiutil.copyFile(image_file, new_cover_file) == nil then
return true
end
end
end
-- custom metadata
--- Returns path to book custom metadata file if it exists, or nil.
function DocSettings:getCustomMetadataFile(doc_path)
doc_path = doc_path or self.data.doc_path
for _, mode in ipairs({"doc", "dir"}) do
local file = self:getSidecarDir(doc_path, mode) .. "/" .. custom_metadata_filename
if lfs.attributes(file, "mode") == "file" then
return file
end
end
end
function DocSettings:openCustomMetadata(custom_metadata_file)
local new = DocSettings:extend{}
local ok, stored
if custom_metadata_file then
ok, stored = pcall(dofile, custom_metadata_file)
end
if ok and next(stored) ~= nil then
new.data = stored
else
new.data = {}
end
new.custom_metadata_file = custom_metadata_file
return new
end
function DocSettings:flushCustomMetadata(doc_path)
local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path)
local new_sidecar_dir
local s_out = dump(self.data, nil, true)
for _, sidecar_dir in ipairs(sidecar_dirs) do
util.makePath(sidecar_dir)
local f_out = io.open(sidecar_dir .. "/" .. custom_metadata_filename, "w")
if f_out ~= nil then
DocSettings.writeFile(f_out, s_out)
new_sidecar_dir = sidecar_dir .. "/"
break
end
end
-- remove old custom metadata file if it was in alternative location
if self.custom_metadata_file then
local old_sidecar_dir = util.splitFilePathName(self.custom_metadata_file)
if old_sidecar_dir ~= new_sidecar_dir then
os.remove(self.custom_metadata_file)
self:removeSidecarDir(doc_path, old_sidecar_dir)
end
end
end
return DocSettings

@ -182,7 +182,7 @@ function Screensaver:expandSpecial(message, fallback)
percent = doc_settings:readSetting("percent_finished") or percent
currentpage = Math.round(percent * totalpages)
percent = Math.round(percent * 100)
props = FileManagerBookInfo.customizeProps(doc_settings:readSetting("doc_props"), lastfile)
props = FileManagerBookInfo.extendProps(doc_settings:readSetting("doc_props"), lastfile)
-- Unable to set time_left_chapter and time_left_document without ReaderUI, so leave N/A
end
if props then

@ -492,7 +492,7 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
end
if loaded then
dbrow.pages = pages
local props = FileManagerBookInfo.customizeProps(document:getProps(), filepath)
local props = FileManagerBookInfo.extendProps(document:getProps(), filepath)
if next(props) then -- there's at least one item
dbrow.has_meta = 'Y'
end

Loading…
Cancel
Save