diff --git a/.ci/helper_luarocks.sh b/.ci/helper_luarocks.sh index a42640253..89c25aad3 100755 --- a/.ci/helper_luarocks.sh +++ b/.ci/helper_luarocks.sh @@ -11,11 +11,8 @@ echo "wrap_bin_scripts = false" >>"${HOME}/.luarocks/config.lua" travis_retry luarocks --local install luafilesystem # for verbose_print module travis_retry luarocks --local install ansicolors -travis_retry luarocks --local install busted 2.0.rc13-0 +travis_retry luarocks --local install busted 2.0.0-1 #- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted -# Apply junit testcase time fix. This can be removed once there is a busted 2.0.rc13 or final -# See https://github.com/Olivine-Labs/busted/commit/830f175c57ca3f9e79f95b8c4eaacf58252453d7 -sed -i 's|testcase_node.time = formatDuration(element.duration)|testcase_node:set_attrib("time", formatDuration(element.duration))|' "${HOME}/.luarocks/share/lua/5.1/busted/outputHandlers/junit.lua" travis_retry luarocks --local install luacheck travis_retry luarocks --local install lanes # for parallel luacheck diff --git a/base b/base index 6924f3541..b00f7f137 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 6924f35412469bf8f82c047e05bd0f9e9ceefa96 +Subproject commit b00f7f1370ec48cd0fb20f662d9974f817aeb6a4 diff --git a/frontend/apps/filemanager/filemanagerbookinfo.lua b/frontend/apps/filemanager/filemanagerbookinfo.lua index 746eec5df..17743e697 100644 --- a/frontend/apps/filemanager/filemanagerbookinfo.lua +++ b/frontend/apps/filemanager/filemanagerbookinfo.lua @@ -157,8 +157,15 @@ function BookInfo:show(file, book_props) local series = book_props.series if series == "" or series == nil then series = _("N/A") - else -- Shorten calibre series decimal number (#4.0 => #4) - series = series:gsub("(#%d+)%.0$", "%1") + else + -- If we were fed a BookInfo book_props (e.g., covermenu), series index is in a separate field + if book_props.series_index then + -- Here, we're assured that series_index is a Lua number, so round integers are automatically displayed without decimals + series = book_props.series .. " #" .. book_props.series_index + else + -- But here, if we have a plain doc_props series with an index, drop empty decimals from round integers. + series = book_props.series:gsub("(#%d+)%.0+$", "%1") + end end table.insert(kv_pairs, { _("Series:"), BD.auto(series) }) diff --git a/frontend/device/kindle/powerd.lua b/frontend/device/kindle/powerd.lua index 15140089a..d82e2d6ef 100644 --- a/frontend/device/kindle/powerd.lua +++ b/frontend/device/kindle/powerd.lua @@ -125,13 +125,6 @@ function KindlePowerD:isChargingHW() return is_charging == 1 end -function KindlePowerD:__gc() - if self.lipc_handle then - self.lipc_handle:close() - self.lipc_handle = nil - end -end - function KindlePowerD:_readFLIntensity() return self:read_int_file(self.fl_intensity_file) end @@ -161,4 +154,12 @@ function KindlePowerD:toggleSuspend() end end +--- @fixme: This won't ever fire, as KindlePowerD is already a metatable on a plain table. +function KindlePowerD:__gc() + if self.lipc_handle then + self.lipc_handle:close() + self.lipc_handle = nil + end +end + return KindlePowerD diff --git a/frontend/document/tilecacheitem.lua b/frontend/document/tilecacheitem.lua index dd8636f4f..7246fe6fa 100644 --- a/frontend/document/tilecacheitem.lua +++ b/frontend/document/tilecacheitem.lua @@ -15,7 +15,7 @@ end function TileCacheItem:dump(filename) logger.dbg("dumping tile cache to", filename, self.excerpt) return serial.dump(self.size, self.excerpt, self.pageno, - self.bb.w, self.bb.h, self.bb.stride, self.bb:getType(), + self.bb.w, self.bb.h, tonumber(self.bb.stride), self.bb:getType(), Blitbuffer.tostring(self.bb), filename) end diff --git a/frontend/ui/renderimage.lua b/frontend/ui/renderimage.lua index 04084b0f3..5061d1f26 100644 --- a/frontend/ui/renderimage.lua +++ b/frontend/ui/renderimage.lua @@ -102,7 +102,7 @@ function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width if want_frames and nb_frames > 1 then -- Returns a regular table, with functions (returning the BlitBuffer) -- as values. Users will have to check via type() and call them. - -- (our luajit does not support __len via metatable, otherwise we + -- (The __len metamethod is a Lua 5.2 feature, otherwise we -- could have used setmetatable to avoid creating all the functions) local frames = {} -- As we don't cache the bb we build on the fly, let caller know it @@ -118,27 +118,30 @@ function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width end) end -- We can't close our GifDocument as long as we may fetch some - -- frame: we need to delay it till 'frames' is no more used. + -- frame: we need to delay it till 'frames' is no longer used. frames.gif_close_needed = true - -- Should happen with that, but __gc seems never called... - frames = setmetatable(frames, { - __gc = function() - logger.dbg("frames.gc() called, closing GifDocument") - if frames.gif_close_needed then - gif:close() - frames.gif_close_needed = nil - end + -- Since frames is a plain table, __gc won't work on Lua 5.1/LuaJIT, + -- not without a little help from the newproxy hack... + frames.gif = gif + local frames_mt = {} + function frames_mt:__gc() + logger.dbg("frames.gc() called, closing GifDocument", self.gif) + if self.gif_close_needed then + self.gif:close() + self.gif_close_needed = nil end - }) - -- so, also set this method, so that ImageViewer can explicitely - -- call it onClose. - frames.free = function() - logger.dbg("frames.free() called, closing GifDocument") - if frames.gif_close_needed then - gif:close() - frames.gif_close_needed = nil + end + -- Much like our other stuff, when we're puzzled about __gc, we do it manually! + -- So, also set this method, so that ImageViewer can explicitely call it onClose. + function frames:free() + logger.dbg("frames.free() called, closing GifDocument", self.gif) + if self.gif_close_needed then + self.gif:close() + self.gif_close_needed = nil end end + local setmetatable = require("ffi/__gc") + setmetatable(frames, frames_mt) return frames else local page = gif:openPage(1) diff --git a/frontend/ui/widget/imageviewer.lua b/frontend/ui/widget/imageviewer.lua index 8c5884f93..cb7b39eb4 100644 --- a/frontend/ui/widget/imageviewer.lua +++ b/frontend/ui/widget/imageviewer.lua @@ -719,7 +719,7 @@ function ImageViewer:onCloseWidget() end -- also clean _images_list if it provides a method for that if self._images_list and self._images_list_disposable and self._images_list.free then - self._images_list.free() + self._images_list:free() end -- NOTE: Assume there's no image beneath us, so, no dithering request UIManager:setDirty(nil, function() diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index d861bf12a..7da41350a 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -205,7 +205,7 @@ function ImageWidget:_loadfile() -- cache this image logger.dbg("cache", hash) cache = ImageCacheItem:new{ bb = self._bb } - cache.size = cache.bb.stride * cache.bb.h + cache.size = tonumber(cache.bb.stride) * cache.bb.h ImageCache:insert(hash, cache) end end diff --git a/frontend/util.lua b/frontend/util.lua index 27001ec99..863ce8495 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -810,11 +810,11 @@ end --- If the given path has a trailing /, returns the entire path as the directory --- path and "" as the file name. ---- @string file ----- @treturn string path, filename +---- @treturn string directory, filename function util.splitFilePathName(file) if file == nil or file == "" then return "", "" end if string.find(file, "/") == nil then return "", file end - return string.gsub(file, "(.*/)(.*)", "%1"), string.gsub(file, ".*/", "") + return file:match("(.*/)(.*)") end --- Splits a file name into its pure file name and suffix @@ -823,7 +823,7 @@ end function util.splitFileNameSuffix(file) if file == nil or file == "" then return "", "" end if string.find(file, "%.") == nil then return file, "" end - return string.gsub(file, "(.*)%.(.*)", "%1"), string.gsub(file, ".*%.", "") + return file:match("(.*)%.(.*)") end --- Gets file extension @@ -835,7 +835,7 @@ function util.getFileNameSuffix(file) end --- Companion helper function that returns the script's language, ---- based on the filme extension. +--- based on the file extension. ---- @string filename ---- @treturn string (lowercase) (or nil if not Device:canExecuteScript(file)) function util.getScriptType(file) diff --git a/plugins/coverbrowser.koplugin/bookinfomanager.lua b/plugins/coverbrowser.koplugin/bookinfomanager.lua index 4e2180244..d1d0d2772 100644 --- a/plugins/coverbrowser.koplugin/bookinfomanager.lua +++ b/plugins/coverbrowser.koplugin/bookinfomanager.lua @@ -4,36 +4,37 @@ local DataStorage = require("datastorage") local Device = require("device") local DocumentRegistry = require("document/documentregistry") local FFIUtil = require("ffi/util") +local InfoMessage = require("ui/widget/infomessage") local RenderImage = require("ui/renderimage") local SQ3 = require("lua-ljsqlite3/init") local UIManager = require("ui/uimanager") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") +local zstd = require("ffi/zstd") local _ = require("gettext") local N_ = _.ngettext local T = FFIUtil.template --- Util functions needed by this plugin, but that may be added to existing base/ffi/ files -local xutil = require("xutil") - -- Database definition -local BOOKINFO_DB_VERSION = "2-20170701" +local BOOKINFO_DB_VERSION = 20201210 local BOOKINFO_DB_SCHEMA = [[ - -- For caching book cover and metadata + -- To cache book cover and metadata CREATE TABLE IF NOT EXISTS bookinfo ( -- Internal book cache id - -- (not to be used to identify a book, it may change for a same book) + -- (not to be used to identify a book, it may change) bcid INTEGER PRIMARY KEY AUTOINCREMENT, -- File location and filename directory TEXT NOT NULL, -- split by dir/name so we can get all files in a directory - filename TEXT NOT NULL, -- and can implement pruning of no more existing files + filename TEXT NOT NULL, -- and can implement pruning of deleted files + filesize INTEGER, -- size in bytes at most recent extraction time + filemtime INTEGER, -- mtime at most recent extraction time -- Extraction status and result - in_progress INTEGER, -- 0 (done), >0 : nb of tries (to avoid re-doing extractions that crashed us) + in_progress INTEGER, -- 0 (done), >0 : nb of tries (to avoid retrying failed extractions forever) unsupported TEXT, -- NULL if supported / reason for being unsupported - cover_fetched TEXT, -- NULL / 'Y' = action of fetching cover was made (whether we got one or not) + cover_fetched TEXT, -- NULL / 'Y' = we tried to fetch a cover (but we may not have gotten one) has_meta TEXT, -- NULL / 'Y' = has metadata (title, authors...) has_cover TEXT, -- NULL / 'Y' = has cover image (cover_*) cover_sizetag TEXT, -- 'M' (Medium, MosaicMenuItem) / 's' (small, ListMenuItem) @@ -50,6 +51,7 @@ local BOOKINFO_DB_SCHEMA = [[ title TEXT, authors TEXT, series TEXT, + series_index REAL, language TEXT, keywords TEXT, description TEXT, @@ -57,26 +59,24 @@ local BOOKINFO_DB_SCHEMA = [[ -- Cover image cover_w INTEGER, -- blitbuffer width cover_h INTEGER, -- blitbuffer height - cover_btype INTEGER, -- blitbuffer type (internal) - cover_bpitch INTEGER, -- blitbuffer pitch (internal) - cover_datalen INTEGER, -- blitbuffer uncompressed data length - cover_dataz BLOB -- blitbuffer data compressed with zlib + cover_bb_type INTEGER, -- blitbuffer type (internal) + cover_bb_stride INTEGER, -- blitbuffer stride (internal) + cover_bb_data BLOB -- blitbuffer data compressed with zstd ); CREATE UNIQUE INDEX IF NOT EXISTS dir_filename ON bookinfo(directory, filename); - -- For keeping track of DB schema version + -- To keep track of CoverBrowser settings CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT ); - -- this will not override previous version value, so we'll get the old one if old schema - INSERT OR IGNORE INTO config VALUES ('version', ']] .. BOOKINFO_DB_VERSION .. [['); - ]] local BOOKINFO_COLS_SET = { "directory", "filename", + "filesize", + "filemtime", "in_progress", "unsupported", "cover_fetched", @@ -89,15 +89,15 @@ local BOOKINFO_COLS_SET = { "title", "authors", "series", + "series_index", "language", "keywords", "description", "cover_w", "cover_h", - "cover_btype", - "cover_bpitch", - "cover_datalen", - "cover_dataz", + "cover_bb_type", + "cover_bb_stride", + "cover_bb_data", } local bookinfo_values_sql = {} -- for "VALUES (?, ?, ?,...)" insert sql part @@ -150,16 +150,35 @@ function BookInfoManager:createDB() -- Less error cases to check if we do it that way -- Create it (noop if already there) db_conn:exec(BOOKINFO_DB_SCHEMA) - -- Check version (not updated by previous exec if already there) - local res = db_conn:exec("SELECT value FROM config where key='version';") - if res[1][1] ~= BOOKINFO_DB_VERSION then - logger.warn("BookInfo cache DB schema updated from version", res[1][1], "to version", BOOKINFO_DB_VERSION) + -- Check version + local db_version = tonumber(db_conn:rowexec("PRAGMA user_version;")) + if db_version < BOOKINFO_DB_VERSION then + logger.warn("BookInfo cache DB schema updated from version", db_version, "to version", BOOKINFO_DB_VERSION) logger.warn("Deleting existing", self.db_location, "to recreate it") + + -- We'll try to preserve settings, though + self:loadSettings(db_conn) + db_conn:close() os.remove(self.db_location) + -- Re-create it db_conn = SQ3.open(self.db_location) db_conn:exec(BOOKINFO_DB_SCHEMA) + + -- Restore non-deprecated settings + for k, v in pairs(self.settings) do + if k ~= "version" then + self:saveSetting(k, v, true) + end + end + self:loadSettings(db_conn) + + -- Update version + db_conn:exec(string.format("PRAGMA user_version=%d;", BOOKINFO_DB_VERSION)) + + -- Say hi! + UIManager:show(InfoMessage:new{text =_("BookInfo cache database schema updated."), timeout = 3 }) end db_conn:close() self.db_created = true @@ -216,19 +235,29 @@ function BookInfoManager:compactDb() end -- Settings management, stored in 'config' table -function BookInfoManager:loadSettings() +function BookInfoManager:loadSettings(db_conn) if lfs.attributes(self.db_location, "mode") ~= "file" then -- no db, empty config self.settings = {} return end self.settings = {} - self:openDbConnection() - local res = self.db_conn:exec("SELECT key, value FROM config;") - local keys = res[1] - local values = res[2] - for i, key in ipairs(keys) do - self.settings[key] = values[i] + + local my_db_conn + if db_conn then + my_db_conn = db_conn + else + self:openDbConnection() + my_db_conn = self.db_conn + end + + local res = my_db_conn:exec("SELECT key, value FROM config;") + if res then + local keys = res[1] + local values = res[2] + for i, key in ipairs(keys) do + self.settings[key] = values[i] + end end end @@ -239,7 +268,7 @@ function BookInfoManager:getSetting(key) return self.settings[key] end -function BookInfoManager:saveSetting(key, value) +function BookInfoManager:saveSetting(key, value, skip_reload) if not value or value == false or value == "" then if lfs.attributes(self.db_location, "mode") ~= "file" then -- If no db created, no need to save (and create db) an empty value @@ -257,8 +286,11 @@ function BookInfoManager:saveSetting(key, value) stmt:bind(key, value) stmt:step() -- commited stmt:clearbind():reset() -- cleanup - -- Reload settings, so we may get (or not if it failed) what we just saved - self:loadSettings() + + -- Optionally, reload settings, so we may get (or not if it failed) what we just saved + if not skip_reload then + self:loadSettings() + end end -- Bookinfo management @@ -274,6 +306,10 @@ function BookInfoManager:getBookInfo(filepath, get_cover) return { directory = directory, filename = filename, + --[[ + filesize = lfs.attributes(filepath, "size"), + filemtime = lfs.attributes(filepath, "modification"), + --]] in_progress = 0, cover_fetched = "Y", has_meta = nil, @@ -289,9 +325,11 @@ function BookInfoManager:getBookInfo(filepath, get_cover) self:openDbConnection() local row = self.get_stmt:bind(directory, filename):step() - self.get_stmt:clearbind():reset() -- get ready for next query + -- NOTE: We do not reset right now because we'll be querying a BLOB, + -- so we need the data it points to to still be there ;). if not row then -- filepath not in db + self.get_stmt:clearbind():reset() -- get ready for next query return nil end @@ -313,16 +351,27 @@ function BookInfoManager:getBookInfo(filepath, get_cover) if bookinfo["has_cover"] then bookinfo["cover_w"] = tonumber(row[num]) bookinfo["cover_h"] = tonumber(row[num+1]) - local cover_data = xutil.zlib_uncompress(row[num+5], row[num+4]) - row[num+5] = nil -- release memory used by cover_dataz - -- Blitbuffer.fromstring() expects : w, h, bb_type, bb_data, pitch - bookinfo["cover_bb"] = Blitbuffer.fromstring(row[num], row[num+1], row[num+2], cover_data, row[num+3]) - -- release memory used by uncompressed data: - cover_data = nil -- luacheck: no unused + local bbtype = tonumber(row[num+2]) + local bbstride = tonumber(row[num+3]) + -- This is a blob_mt table! Essentially, a (ptr, size) tuple. + local cover_blob = row[num+4] + -- The pointer returned by SQLite is only valid until the next step/reset/finalize! + -- (which means its memory management is entirely in the hands of SQLite) + local cover_data, cover_size = zstd.zstd_uncompress_ctx(cover_blob[1], cover_blob[2]) + -- Double-check that the size of the uncompressed BB is as expected... + local expected_cover_size = bbstride * bookinfo["cover_h"] + assert(cover_size == expected_cover_size, "Uncompressed a " .. tonumber(cover_size) .. "b BB instead of the expected " .. tonumber(expected_cover_size) .. "b") + -- That one, on the other hand, is on the heap, so we can use it without making a copy. + local cover_bb = Blitbuffer.new(bookinfo["cover_w"], bookinfo["cover_h"], bbtype, cover_data, bbstride, bookinfo["cover_w"]) + -- Mark its data pointer as safe to free() on GC + cover_bb:setAllocated(1) + bookinfo["cover_bb"] = cover_bb end break end end + + self.get_stmt:clearbind():reset() -- get ready for next query return bookinfo end @@ -395,6 +444,11 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs) return -- Last insert done for this book, we're giving up end + -- Update this on each extraction attempt. Might be useful to reset the counter in case file gets updated. + local file_attr = lfs.attributes(filepath) + dbrow.filesize = file_attr.size + dbrow.filemtime = file_attr.modification + -- Proceed with extracting info local document = DocumentRegistry:openDocument(filepath) local loaded = true @@ -423,7 +477,28 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs) end if props.title and props.title ~= "" then dbrow.title = props.title end if props.authors and props.authors ~= "" then dbrow.authors = props.authors end - if props.series and props.series ~= "" then dbrow.series = props.series end + if props.series and props.series ~= "" then + -- NOTE: If there's a series index in there, split it off to series_index, and only store the name in series. + -- This property is currently only set by: + -- * DjVu, for which I couldn't find a real standard for metadata fields + -- (we currently use Series for this field, c.f., https://exiftool.org/TagNames/DjVu.html). + -- * CRe, which could offer us a split getSeriesName & getSeriesNumber... + -- except getSeriesNumber does an atoi, so it'd murder decimal values. + -- So, instead, parse how it formats the whole thing as a string ;). + if string.find(props.series, "#") then + dbrow.series, dbrow.series_index = props.series:match("(.*) #(%d+%.?%d-)$") + if dbrow.series_index then + -- We're inserting via a bind method, so make sure we feed it a Lua number, because it's a REAL in the db. + dbrow.series_index = tonumber(dbrow.series_index) + else + -- If the index pattern didn't match (e.g., nothing after the octothorp, or a string), + -- restore the full thing as the series name. + dbrow.series = props.series + end + else + dbrow.series = props.series + end + end if props.language and props.language ~= "" then dbrow.language = props.language end if props.keywords and props.keywords ~= "" then dbrow.keywords = props.keywords end if props.description and props.description ~= "" then dbrow.description = props.description end @@ -447,18 +522,16 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs) cbb_h = math.min(math.floor(cbb_h * scale_factor)+1, spec_max_cover_h) cover_bb = RenderImage:scaleBlitBuffer(cover_bb, cbb_w, cbb_h, true) end - dbrow.cover_w = cbb_w - dbrow.cover_h = cbb_h - dbrow.cover_btype = cover_bb:getType() - dbrow.cover_bpitch = cover_bb.stride - local cover_data = Blitbuffer.tostring(cover_bb) - cover_bb:free() -- free bb before compressing to save memory - dbrow.cover_datalen = cover_data:len() - local cover_dataz = xutil.zlib_compress(cover_data) - -- release memory used by uncompressed data: - cover_data = nil -- luacheck: no unused - dbrow.cover_dataz = SQ3.blob(cover_dataz) -- cast to blob for sqlite - logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cbb_w, "x", cbb_h, ", compressed from", dbrow.cover_datalen, "to", cover_dataz:len()) + dbrow.cover_w = cover_bb.w + dbrow.cover_h = cover_bb.h + dbrow.cover_bb_type = cover_bb:getType() + dbrow.cover_bb_stride = tonumber(cover_bb.stride) + local cover_size = cover_bb.stride * cover_bb.h + local cover_zst_ptr, cover_zst_size = zstd.zstd_compress(cover_bb.data, cover_size) + dbrow.cover_bb_data = SQ3.blob(cover_zst_ptr, cover_zst_size) -- cast to blob for sqlite + logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cover_bb.w, "x", cover_bb.h, ", compressed from", tonumber(cover_size), "to", tonumber(cover_zst_size)) + -- We're done with the uncompressed bb now, and the compressed one has been managed by SQLite ;) + cover_bb:free() end end end @@ -684,7 +757,6 @@ end -- Batch extraction function BookInfoManager:extractBooksInDirectory(path, cover_specs) local Geom = require("ui/geometry") - local InfoMessage = require("ui/widget/infomessage") local TopContainer = require("ui/widget/container/topcontainer") local Trapper = require("ui/trapper") local Screen = require("device").screen diff --git a/plugins/coverbrowser.koplugin/listmenu.lua b/plugins/coverbrowser.koplugin/listmenu.lua index 0778496fc..598d1b547 100644 --- a/plugins/coverbrowser.koplugin/listmenu.lua +++ b/plugins/coverbrowser.koplugin/listmenu.lua @@ -540,9 +540,11 @@ function ListMenuItem:update() end -- add Series metadata if requested if bookinfo.series then - -- Shorten calibre series decimal number (#4.0 => #4) - bookinfo.series = bookinfo.series:gsub("(#%d+)%.0$", "%1") - bookinfo.series = BD.auto(bookinfo.series) + if bookinfo.series_index then + bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index) + else + bookinfo.series = BD.auto(bookinfo.series) + end if series_mode == "append_series_to_title" then if title then title = title .. " - " .. bookinfo.series diff --git a/plugins/coverbrowser.koplugin/mosaicmenu.lua b/plugins/coverbrowser.koplugin/mosaicmenu.lua index c82f4cd30..3825625c6 100644 --- a/plugins/coverbrowser.koplugin/mosaicmenu.lua +++ b/plugins/coverbrowser.koplugin/mosaicmenu.lua @@ -595,9 +595,11 @@ function MosaicMenuItem:update() local series_mode = BookInfoManager:getSetting("series_mode") local title_add, authors_add if bookinfo.series then - -- Shorten calibre series decimal number (#4.0 => #4) - bookinfo.series = bookinfo.series:gsub("(#%d+)%.0$", "%1") - bookinfo.series = BD.auto(bookinfo.series) + if bookinfo.series_index then + bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index) + else + bookinfo.series = BD.auto(bookinfo.series) + end if series_mode == "append_series_to_title" then if bookinfo.title then title_add = " - " .. bookinfo.series diff --git a/plugins/coverbrowser.koplugin/xutil.lua b/plugins/coverbrowser.koplugin/xutil.lua deleted file mode 100644 index e5cc6d83a..000000000 --- a/plugins/coverbrowser.koplugin/xutil.lua +++ /dev/null @@ -1,35 +0,0 @@ -local ffi = require("ffi") - --- Utilities functions needed by this plugin, but that may be added to --- existing base/ffi/ files -local xutil = {} - --- Data compression/decompression of strings thru zlib (may be put in a new base/ffi/zlib.lua) --- from http://luajit.org/ext_ffi_tutorial.html -ffi.cdef[[ -unsigned long compressBound(unsigned long sourceLen); -int compress2(uint8_t *dest, unsigned long *destLen, - const uint8_t *source, unsigned long sourceLen, int level); -int uncompress(uint8_t *dest, unsigned long *destLen, - const uint8_t *source, unsigned long sourceLen); -]] -local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z") - -function xutil.zlib_compress(data) - local n = zlib.compressBound(#data) - local buf = ffi.new("uint8_t[?]", n) - local buflen = ffi.new("unsigned long[1]", n) - local res = zlib.compress2(buf, buflen, data, #data, 9) - assert(res == 0) - return ffi.string(buf, buflen[0]) -end - -function xutil.zlib_uncompress(zdata, datalen) - local buf = ffi.new("uint8_t[?]", datalen) - local buflen = ffi.new("unsigned long[1]", datalen) - local res = zlib.uncompress(buf, buflen, zdata, #zdata) - assert(res == 0) - return ffi.string(buf, buflen[0]) -end - -return xutil diff --git a/reader.lua b/reader.lua index e66d94fd4..89fb1aaac 100755 --- a/reader.lua +++ b/reader.lua @@ -53,7 +53,9 @@ if G_reader_settings:isTrue("debug") and G_reader_settings:isTrue("debug_verbose -- Option parsing: local longopts = { debug = "d", + verbose = "d", profile = "p", + teardown = "t", help = "h", } @@ -64,6 +66,7 @@ local function showusage() print("-d start in debug mode") print("-v debug in verbose mode") print("-p enable Lua code profiling") + print("-t teardown via a return instead of calling os.exit") print("-h show this usage help") print("") print("If you give the name of a directory instead of a file path, a file") @@ -76,6 +79,7 @@ local function showusage() end local Profiler = nil +local sane_teardown local ARGV = arg local argidx = 1 while argidx <= #ARGV do @@ -97,6 +101,8 @@ while argidx <= #ARGV do elseif arg == "-p" then Profiler = require("jit.p") Profiler.start("la") + elseif arg == "-t" then + sane_teardown = true else -- not a recognized option, should be a filename argidx = argidx - 1 @@ -335,10 +341,20 @@ local function exitReader() if Profiler then Profiler.stop() end if type(exit_code) == "number" then - os.exit(exit_code) + return exit_code else - os.exit(0) + return 0 end end -exitReader() +local ret = exitReader() + +if not sane_teardown then + os.exit(ret) +else + -- NOTE: We can unfortunately not return with a custom exit code... + -- But since this should only really be used on the emulator, + -- to ensure a saner teardown of ressources when debugging, + -- it's not a great loss... + return +end diff --git a/spec/unit/cache_spec.lua b/spec/unit/cache_spec.lua index a502e1e2b..3e8dc7d08 100644 --- a/spec/unit/cache_spec.lua +++ b/spec/unit/cache_spec.lua @@ -10,6 +10,9 @@ describe("Cache module", function() local sample_pdf = "spec/front/unit/data/sample.pdf" doc = DocumentRegistry:openDocument(sample_pdf) end) + teardown(function() + doc:close() + end) it("should clear cache", function() Cache:clear() diff --git a/spec/unit/evernote_plugin_main_spec.lua b/spec/unit/evernote_plugin_main_spec.lua index 4bdbe80ca..eef4aa40e 100644 --- a/spec/unit/evernote_plugin_main_spec.lua +++ b/spec/unit/evernote_plugin_main_spec.lua @@ -68,6 +68,9 @@ describe("Evernote plugin module", function() } end) + teardown(function() + readerui:onClose() + end) it("should write clippings to txt file", function () local file_mock = mock( { @@ -101,4 +104,4 @@ describe("Evernote plugin module", function() end) -end) \ No newline at end of file +end) diff --git a/spec/unit/readerbookmark_spec.lua b/spec/unit/readerbookmark_spec.lua index c03583f22..535e340ee 100644 --- a/spec/unit/readerbookmark_spec.lua +++ b/spec/unit/readerbookmark_spec.lua @@ -53,6 +53,10 @@ describe("ReaderBookmark module", function() } readerui.status.enabled = false end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) before_each(function() UIManager:quit() UIManager:show(readerui) @@ -136,6 +140,10 @@ describe("ReaderBookmark module", function() } readerui.status.enabled = false end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) before_each(function() UIManager:quit() UIManager:show(readerui) diff --git a/spec/unit/readerdictionary_spec.lua b/spec/unit/readerdictionary_spec.lua index c2f84a25f..5902d90fa 100644 --- a/spec/unit/readerdictionary_spec.lua +++ b/spec/unit/readerdictionary_spec.lua @@ -19,6 +19,10 @@ describe("Readerdictionary module", function() rolling = readerui.rolling dictionary = readerui.dictionary end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) it("should show quick lookup window", function() local name = "screenshots/reader_dictionary.png" UIManager:quit() diff --git a/spec/unit/readerfooter_spec.lua b/spec/unit/readerfooter_spec.lua index c1ca6f318..ac512fed1 100644 --- a/spec/unit/readerfooter_spec.lua +++ b/spec/unit/readerfooter_spec.lua @@ -89,6 +89,8 @@ describe("Readerfooter module", function() } assert.is.same(true, readerui.view.footer_visible) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should setup footer as visible not in all_at_once", function() @@ -116,6 +118,8 @@ describe("Readerfooter module", function() assert.is.same(true, readerui.view.footer_visible) assert.is.same(1, readerui.view.footer.mode, 1) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should setup footer as invisible in full screen mode", function() @@ -133,6 +137,8 @@ describe("Readerfooter module", function() } assert.is.same(false, readerui.view.footer_visible) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should setup footer as visible in mini progress bar mode", function() @@ -150,6 +156,8 @@ describe("Readerfooter module", function() } assert.is.same(false, readerui.view.footer_visible) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should setup footer as invisible", function() @@ -167,6 +175,8 @@ describe("Readerfooter module", function() } assert.is.same(true, readerui.view.footer_visible) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should setup footer for epub without error", function() @@ -186,6 +196,8 @@ describe("Readerfooter module", function() -- c.f., NOTE above, Statistics are disabled, hence the N/A results assert.are.same('1 / '..page_count..' | '..timeinfo..' | ⇒ 0 | 0% | ⤠ 0% | ⏳ N/A | ⤻ N/A', footer.footer_text.text) + readerui:closeDocument() + readerui:onClose() end) it("should setup footer for pdf without error", function() @@ -202,6 +214,8 @@ describe("Readerfooter module", function() local timeinfo = readerui.view.footer.textGeneratorMap.time(footer) assert.are.same('1 / 2 | '..timeinfo..' | ⇒ 1 | 0% | ⤠ 50% | ⏳ N/A | ⤻ N/A', readerui.view.footer.footer_text.text) + readerui:closeDocument() + readerui:onClose() end) it("should switch between different modes", function() @@ -257,6 +271,8 @@ describe("Readerfooter module", function() -- reenable chapter time to read, text should be chapter time to read tapFooterMenu(fake_menu, "Chapter time to read".." (⤻)") assert.are.same('⤻ N/A', footer.footer_text.text) + readerui:closeDocument() + readerui:onClose() end) it("should rotate through different modes", function() @@ -296,6 +312,8 @@ describe("Readerfooter module", function() -- Make it visible again to make the following tests behave... footer:onTapFooter() assert.is.same(1, footer.mode) + readerui:closeDocument() + readerui:onClose() end) it("should pick up screen resize in resetLayout", function() @@ -330,6 +348,8 @@ describe("Readerfooter module", function() expected = is_am() and 518 or 510 assert.is.same(expected, footer.progress_bar.width) Screen.getWidth = old_screen_getwidth + readerui:closeDocument() + readerui:onClose() end) it("should update width on PosUpdate event", function() @@ -353,6 +373,8 @@ describe("Readerfooter module", function() assert.are.same(expected, footer.progress_bar.width) expected = is_am() and 394 or 402 assert.are.same(expected, footer.text_width) + readerui:closeDocument() + readerui:onClose() end) it("should support chapter markers", function() @@ -378,6 +400,8 @@ describe("Readerfooter module", function() footer.settings.toc_markers = false footer:setTocMarkers() assert.are.same(nil, footer.progress_bar.ticks) + readerui:closeDocument() + readerui:onClose() end) it("should schedule/unschedule auto refresh time task", function() @@ -412,6 +436,8 @@ describe("Readerfooter module", function() end end assert.is.same(0, found) + readerui:closeDocument() + readerui:onClose() end) it("should not schedule auto refresh time task if footer is disabled", function() @@ -438,6 +464,8 @@ describe("Readerfooter module", function() end end assert.is.same(0, found) + readerui:closeDocument() + readerui:onClose() end) it("should toggle auto refresh time task by toggling the menu", function() @@ -487,6 +515,8 @@ describe("Readerfooter module", function() end end assert.is.same(1, found) + readerui:closeDocument() + readerui:onClose() end) it("should support toggle footer through menu if tap zone is disabled", function() @@ -532,6 +562,8 @@ describe("Readerfooter module", function() assert.is.same(2, footer.mode) DTAP_ZONE_MINIBAR = saved_tap_zone_minibar --luacheck: ignore + readerui:closeDocument() + readerui:onClose() end) it("should remove and add modes to footer text in all_at_once mode", function() @@ -563,6 +595,8 @@ describe("Readerfooter module", function() -- add mode to footer text tapFooterMenu(fake_menu, "Progress percentage".." (⤠)") assert.are.same('1 / 2 | ⤠ 50%', footer.footer_text.text) + readerui:closeDocument() + readerui:onClose() end) it("should initialize text mode in all_at_once mode", function() @@ -587,6 +621,8 @@ describe("Readerfooter module", function() assert.is.truthy(footer.settings.all_at_once) assert.is.truthy(0, footer.mode) assert.is.falsy(readerui.view.footer_visible) + readerui:closeDocument() + readerui:onClose() end) it("should support disabling all the modes", function() @@ -624,6 +660,8 @@ describe("Readerfooter module", function() assert.is.same(true, footer.has_no_mode) tapFooterMenu(fake_menu, "Progress percentage".." (⤠)") assert.is.same(false, footer.has_no_mode) + readerui:closeDocument() + readerui:onClose() end) it("should return correct footer height in time mode", function() @@ -643,6 +681,8 @@ describe("Readerfooter module", function() assert.falsy(footer.has_no_mode) assert.truthy(readerui.view.footer_visible) assert.is.same(15, footer:getHeight()) + readerui:closeDocument() + readerui:onClose() end) it("should return correct footer height when all modes are disabled", function() @@ -662,6 +702,8 @@ describe("Readerfooter module", function() assert.truthy(footer.has_no_mode) assert.truthy(readerui.view.footer_visible) assert.is.same(15, footer:getHeight()) + readerui:closeDocument() + readerui:onClose() end) it("should disable footer when all modes + progressbar are disabled", function() @@ -680,6 +722,8 @@ describe("Readerfooter module", function() assert.truthy(footer.has_no_mode) assert.falsy(readerui.view.footer_visible) + readerui:closeDocument() + readerui:onClose() end) it("should disable footer if settings.disabled is true", function() @@ -698,6 +742,8 @@ describe("Readerfooter module", function() assert.falsy(readerui.view.footer_visible) assert.truthy(footer.onCloseDocument == nil) assert.truthy(footer.mode == 0) + readerui:closeDocument() + readerui:onClose() end) it("should toggle between full and min progress bar for cre documents", function() @@ -723,5 +769,7 @@ describe("Readerfooter module", function() readerui.rolling:onSetStatusLine(0) assert.is.same(0, footer.mode) assert.falsy(readerui.view.footer_visible) + readerui:closeDocument() + readerui:onClose() end) end) diff --git a/spec/unit/readerhighlight_spec.lua b/spec/unit/readerhighlight_spec.lua index 4f4f2a7dd..c621e15cb 100644 --- a/spec/unit/readerhighlight_spec.lua +++ b/spec/unit/readerhighlight_spec.lua @@ -74,6 +74,10 @@ describe("Readerhighlight module", function() document = DocumentRegistry:openDocument(sample_epub), } end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) before_each(function() UIManager:quit() readerui.rolling:onGotoPage(page) @@ -117,6 +121,10 @@ describe("Readerhighlight module", function() } readerui:handleEvent(Event:new("SetScrollMode", false)) end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) describe("for scanned page with text layer", function() before_each(function() UIManager:quit() @@ -201,6 +209,10 @@ describe("Readerhighlight module", function() } readerui:handleEvent(Event:new("SetScrollMode", true)) end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) describe("for scanned page with text layer", function() before_each(function() UIManager:quit() diff --git a/spec/unit/readerlink_spec.lua b/spec/unit/readerlink_spec.lua index 28b36f6c3..98786961d 100644 --- a/spec/unit/readerlink_spec.lua +++ b/spec/unit/readerlink_spec.lua @@ -22,6 +22,8 @@ describe("ReaderLink module", function() readerui.rolling:onGotoPage(5) readerui.link:onTap(nil, {pos = {x = 320, y = 190}}) assert.is.same(37, readerui.rolling.current_page) + readerui:closeDocument() + readerui:onClose() end) it("should jump to links in pdf page mode", function() @@ -36,6 +38,8 @@ describe("ReaderLink module", function() readerui.link:onTap(nil, {pos = {x = 363, y = 565}}) UIManager:run() assert.is.same(22, readerui.paging.current_page) + readerui:closeDocument() + readerui:onClose() end) it("should jump to links in pdf scroll mode", function() @@ -54,6 +58,8 @@ describe("ReaderLink module", function() -- page positions may have unexpected impact on page number assert.truthy(readerui.paging.current_page == 21 or readerui.paging.current_page == 20) + readerui:closeDocument() + readerui:onClose() end) it("should be able to go back after link jump in epub #nocov", function() @@ -66,6 +72,8 @@ describe("ReaderLink module", function() assert.is.same(37, readerui.rolling.current_page) readerui.link:onGoBackLink() assert.is.same(5, readerui.rolling.current_page) + readerui:closeDocument() + readerui:onClose() end) it("should be able to go back after link jump in pdf page mode", function() @@ -82,6 +90,8 @@ describe("ReaderLink module", function() assert.is.same(22, readerui.paging.current_page) readerui.link:onGoBackLink() assert.is.same(1, readerui.paging.current_page) + readerui:closeDocument() + readerui:onClose() end) it("should be able to go back after link jump in pdf scroll mode", function() @@ -100,6 +110,8 @@ describe("ReaderLink module", function() or readerui.paging.current_page == 20) readerui.link:onGoBackLink() assert.is.same(1, readerui.paging.current_page) + readerui:closeDocument() + readerui:onClose() end) it("should be able to go back to the same position after link jump in pdf scroll mode", function() @@ -175,5 +187,7 @@ describe("ReaderLink module", function() readerui.link:onGoBackLink() assert.is.same(3, readerui.paging.current_page) assert.are.same(expected_page_states, readerui.view.page_states) + readerui:closeDocument() + readerui:onClose() end) end) diff --git a/spec/unit/readerpaging_spec.lua b/spec/unit/readerpaging_spec.lua index c49185f9b..6c35694fd 100644 --- a/spec/unit/readerpaging_spec.lua +++ b/spec/unit/readerpaging_spec.lua @@ -20,6 +20,10 @@ describe("Readerpaging module", function() } paging = readerui.paging end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) it("should emit EndOfBook event at the end", function() UIManager:quit() @@ -53,6 +57,10 @@ describe("Readerpaging module", function() } paging = readerui.paging end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) it("should emit EndOfBook event at the end", function() UIManager:quit() @@ -81,6 +89,8 @@ describe("Readerpaging module", function() document = DocumentRegistry:openDocument(sample_djvu), } tmp_readerui.paging:onScrollPanRel(-100) + tmp_readerui:closeDocument() + tmp_readerui:onClose() end) it("should scroll forward on the last page without crash", function() @@ -94,6 +104,8 @@ describe("Readerpaging module", function() paging:onScrollPanRel(120) paging:onScrollPanRel(-1) paging:onScrollPanRel(120) + tmp_readerui:closeDocument() + tmp_readerui:onClose() end) end) end) diff --git a/spec/unit/readerrolling_spec.lua b/spec/unit/readerrolling_spec.lua index 47ba387c0..daddf056c 100644 --- a/spec/unit/readerrolling_spec.lua +++ b/spec/unit/readerrolling_spec.lua @@ -106,6 +106,8 @@ describe("Readerrolling module", function() txt_rolling:onGotoViewRel(1) assert.is.truthy(called) readerui.onEndOfBook = nil + txt_readerui:closeDocument() + txt_readerui:onClose() end) end) @@ -205,10 +207,14 @@ describe("Readerrolling module", function() end local test_book = "spec/front/unit/data/sample.txt" require("docsettings"):open(test_book):purge() - ReaderUI:new{ + local tmp_readerui = ReaderUI:new{ document = DocumentRegistry:openDocument(test_book), } ReaderView.onPageUpdate = saved_handler + tmp_readerui:closeDocument() + tmp_readerui:onClose() + readerui:closeDocument() + readerui:onClose() end) end) end) diff --git a/spec/unit/readersearch_spec.lua b/spec/unit/readersearch_spec.lua index aa31fcd58..dedbfbb9a 100644 --- a/spec/unit/readersearch_spec.lua +++ b/spec/unit/readersearch_spec.lua @@ -12,9 +12,9 @@ describe("Readersearch module", function() end) describe("search API for EPUB documents", function() - local doc, search, rolling + local readerui, doc, search, rolling setup(function() - local readerui = ReaderUI:new{ + readerui = ReaderUI:new{ dimen = Screen:getSize(), document = DocumentRegistry:openDocument(sample_epub), } @@ -22,6 +22,10 @@ describe("Readersearch module", function() search = readerui.search rolling = readerui.rolling end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) it("should search backward", function() rolling:onGotoPage(10) assert.truthy(search:searchFromCurrent("Verona", 1)) @@ -117,9 +121,9 @@ describe("Readersearch module", function() end) describe("search API for PDF documents", function() - local doc, search, paging + local readerui, doc, search, paging setup(function() - local readerui = ReaderUI:new{ + readerui = ReaderUI:new{ dimen = Screen:getSize(), document = DocumentRegistry:openDocument(sample_pdf), } @@ -127,6 +131,10 @@ describe("Readersearch module", function() search = readerui.search paging = readerui.paging end) + teardown(function() + readerui:closeDocument() + readerui:onClose() + end) it("should match single word with case insensitive option in one page", function() assert.are.equal(9, #doc.koptinterface:findAllMatches(doc, "what", true, 20)) assert.are.equal(51, #doc.koptinterface:findAllMatches(doc, "the", true, 20)) diff --git a/spec/unit/readertoc_spec.lua b/spec/unit/readertoc_spec.lua index e7b0b7614..58f348852 100644 --- a/spec/unit/readertoc_spec.lua +++ b/spec/unit/readertoc_spec.lua @@ -10,6 +10,12 @@ describe("Readertoc module", function() DEBUG = require("dbg") local sample_epub = "spec/front/unit/data/juliet.epub" + -- Clear settings from previous tests + local DocSettings = require("docsettings") + local doc_settings = DocSettings:open(sample_epub) + doc_settings:close() + doc_settings:purge() + readerui = ReaderUI:new{ dimen = Screen:getSize(), document = DocumentRegistry:openDocument(sample_epub), @@ -97,6 +103,10 @@ describe("Readertoc module", function() assert.are.same(12, #toc.collapsed_toc) toc:collapseToc(18) assert.are.same(7, #toc.collapsed_toc) + + --- @note: Delay the teardown 'til the last test, because of course the tests rely on incremental state changes across tests... + readerui:closeDocument() + readerui:onClose() end) end) end) diff --git a/spec/unit/readerui_spec.lua b/spec/unit/readerui_spec.lua index f57a9e77a..289128e98 100644 --- a/spec/unit/readerui_spec.lua +++ b/spec/unit/readerui_spec.lua @@ -36,6 +36,7 @@ describe("Readerui module", function() it("should close document", function() readerui:closeDocument() assert(readerui.document == nil) + readerui:onClose() end) it("should not reset running_instance by mistake", function() ReaderUI:doShowReader(sample_epub) @@ -46,6 +47,7 @@ describe("Readerui module", function() document = DocumentRegistry:openDocument(sample_epub) }:onClose() assert.is.truthy(new_readerui.document) + new_readerui:closeDocument() new_readerui:onClose() end) end) diff --git a/spec/unit/readerview_spec.lua b/spec/unit/readerview_spec.lua index 9495d118e..bc06153a8 100644 --- a/spec/unit/readerview_spec.lua +++ b/spec/unit/readerview_spec.lua @@ -47,6 +47,10 @@ describe("Readerview module", function() error("UIManager's task queue should be emtpy.") end end + + if readerui.document then + readerui:closeDocument() + end end) it("should return and restore view context in page mode", function() @@ -99,6 +103,8 @@ describe("Readerview module", function() assert.is.same(view.visible_area.x, 0) assert.is.same(view.visible_area.y, 10) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) it("should return and restore view context in scroll mode", function() @@ -152,5 +158,7 @@ describe("Readerview module", function() assert.is.same(view.page_states[1].visible_area.x, 0) assert.is.same(view.page_states[1].visible_area.y, 10) G_reader_settings:delSetting("reader_footer_mode") + readerui:closeDocument() + readerui:onClose() end) end) diff --git a/spec/unit/screenshoter_spec.lua b/spec/unit/screenshoter_spec.lua index ce2d94b95..dbe53623f 100644 --- a/spec/unit/screenshoter_spec.lua +++ b/spec/unit/screenshoter_spec.lua @@ -19,6 +19,8 @@ describe("ReaderScreenshot module", function() teardown(function() readerui:handleEvent(Event:new("SetRotationMode", Screen.ORIENTATION_PORTRAIT)) + readerui:closeDocument() + readerui:onClose() end) it("should get screenshot in portrait", function()