2
0
mirror of https://github.com/koreader/koreader synced 2024-11-11 19:11:14 +00:00
koreader/plugins/calibre.koplugin/metadata.lua

253 lines
7.1 KiB
Lua
Raw Normal View History

2020-10-12 15:20:16 +00:00
--[[--
This module implements functions for loading, saving and editing calibre metadata files.
2020-10-12 15:20:16 +00:00
Calibre uses JSON to store metadata on device after each wired transfer.
In wireless transfers calibre sends the same metadata to the client, which is in charge
of storing it.
@module koplugin.calibre.metadata
--]]--
local rapidjson = require("rapidjson")
local logger = require("logger")
local util = require("util")
local unused_metadata = {
"application_id",
"author_link_map",
"author_sort",
"author_sort_map",
"book_producer",
"comments",
"cover",
"db_id",
"identifiers",
"languages",
"pubdate",
"publication_type",
"publisher",
"rating",
"rights",
"thumbnail",
"timestamp",
"title_sort",
"user_categories",
"user_metadata",
"_series_sort_",
}
--- find calibre files for a given dir
local function findCalibreFiles(dir)
local function existOrLast(file)
local fullname
local options = { file, "." .. file }
for _, option in pairs(options) do
fullname = dir .. "/" .. option
if util.fileExists(fullname) then
return true, fullname
end
end
return false, fullname
end
local ok_meta, file_meta = existOrLast("metadata.calibre")
local ok_drive, file_drive = existOrLast("driveinfo.calibre")
return ok_meta, ok_drive, file_meta, file_drive
end
local CalibreMetadata = {
-- info about the library itself. It should
-- hold a table with the contents of "driveinfo.calibre"
drive = {},
-- info about the books in this library. It should
-- hold a table with the contents of "metadata.calibre"
books = {},
}
--- loads driveinfo from JSON file
function CalibreMetadata:loadDeviceInfo(file)
if not file then file = self.driveinfo end
local json, err = rapidjson.load(file)
if not json then
logger.warn("Unable to load device info from JSON file:", err)
return {}
end
return json
end
-- saves driveinfo to JSON file
function CalibreMetadata:saveDeviceInfo(arg)
-- keep previous device name. This allow us to identify the calibre driver used.
-- "Folder" is used by connect to folder
-- "KOReader" is used by smart device app
-- "Amazon", "Kobo", "Bq" ... are used by platform device drivers
local previous_name = self.drive.device_name
self.drive = arg
if previous_name then
self.drive.device_name = previous_name
end
rapidjson.dump(self.drive, self.driveinfo)
end
-- loads books' metadata from JSON file
function CalibreMetadata:loadBookList()
local json, err = rapidjson.load(self.metadata)
if not json then
logger.warn("Unable to load book list from JSON file:", self.metadata, err)
return {}
end
return json
end
-- saves books' metadata to JSON file
function CalibreMetadata:saveBookList()
-- replace bad table values with null
local file = self.metadata
local books = self.books
for index, book in ipairs(books) do
for key, item in pairs(book) do
if type(item) == "function" then
books[index][key] = rapidjson.null
end
end
end
rapidjson.dump(rapidjson.array(books), file, { pretty = true })
end
-- add a book to our books table
function CalibreMetadata:addBook(metadata)
for _, key in pairs(unused_metadata) do
metadata[key] = nil
end
table.insert(self.books, #self.books + 1, metadata)
end
-- remove a book from our books table
function CalibreMetadata:removeBook(lpath)
for index, book in ipairs(self.books) do
if book.lpath == lpath then
table.remove(self.books, index)
end
end
end
-- gets the uuid and index of a book from its path
function CalibreMetadata:getBookUuid(lpath)
for index, book in ipairs(self.books) do
if book.lpath == lpath then
return book.uuid, index
end
end
return "none"
end
-- gets the book id at the given index
function CalibreMetadata:getBookId(index)
local book = {}
book.priKey = index
for _, key in pairs({ "uuid", "lpath", "last_modified"}) do
book[key] = self.books[index][key]
end
return book
end
-- gets the book metadata at the given index
function CalibreMetadata:getBookMetadata(index)
local book = self.books[index]
for key, value in pairs(book) do
if type(value) == "function" then
book[key] = rapidjson.null
end
end
return book
end
-- removes deleted books from table
function CalibreMetadata:prune()
local count = 0
for index, book in ipairs(self.books) do
local path = self.path .. "/" .. book.lpath
if not util.fileExists(path) then
logger.dbg("prunning book from DB at index", index, "path", path)
self:removeBook(book.lpath)
count = count + 1
end
end
if count > 0 then
self:saveBookList()
end
return count
end
-- removes unused metadata from books
function CalibreMetadata:cleanUnused()
local slim_books = self.books
for index, _ in ipairs(slim_books) do
for _, key in pairs(unused_metadata) do
slim_books[index][key] = nil
end
end
self.books = slim_books
self:saveBookList()
end
-- cleans all temp data stored for current library.
function CalibreMetadata:clean()
self.books = {}
self.drive = {}
self.path = nil
self.driveinfo = nil
self.metadata = nil
end
-- get keys from driveinfo.calibre
function CalibreMetadata:getDeviceInfo(dir, kind)
if not dir or not kind then return end
local _, ok_drive, __, driveinfo = findCalibreFiles(dir)
if not ok_drive then return end
local drive = self:loadDeviceInfo(driveinfo)
if drive then
return drive[kind]
end
end
-- initialize a directory as a calibre library.
-- This is the main function. Call it to initialize a calibre library
-- in a given path. It will find calibre files if they're on disk and
-- try to load info from them.
-- NOTE: you should care about the books table, because it could be huge.
-- If you're not working with the metadata directly (ie: in wireless connections)
-- you should copy relevant data to another table and free this one to keep things tidy.
function CalibreMetadata:init(dir, is_search)
if not dir then return end
local socket = require("socket")
local start = socket.gettime()
self.path = dir
local ok_meta, ok_drive, file_meta, file_drive = findCalibreFiles(dir)
self.driveinfo = file_drive
if ok_drive then
self.drive = self:loadDeviceInfo()
end
self.metadata = file_meta
if ok_meta then
self.books = self:loadBookList()
elseif is_search then
-- no metadata to search
return false
end
local deleted_count = self:prune()
local elapsed = socket.gettime() - start
logger.info(string.format(
"calibre info loaded from disk in %f milliseconds: %d books. %d pruned",
elapsed * 1000, #self.books, deleted_count))
if not is_search then
self:cleanUnused()
end
return true
end
return CalibreMetadata