mirror of
https://github.com/koreader/koreader
synced 2024-11-11 19:11:14 +00:00
83cde64bcc
joins calibre metadata search and calibre wireless connections into a single plugin search metadata changes: - search directly into calibre metadata files. - search can be performed on more than one library (configurable from a menu) - device scans now find all calibre libraries under a given root - search options can be configured from a menu. (case sensitive, find by title, author and path) - removed legacy global variables. - *option* to search from the reader - *option* to generate a cache of books for faster searches. calibre wireless connection changes: - keep track of books in a library (includes prunning books from calibre metadata if the file was deleted locally) - remove files on device from calibre - support password protected connections - FM integration: if we're in the inbox dir it will be updated each time a book is added or deleted. - disconnect when requested by calibre, available on newer calibre versions (+4.17) - remove unused opcodes. - better report of client name, version and device id - free disk space checks for all calibre versions - bump supported extensions to match what KOReader can handle. Users can override this with their own list of extensions (or from calibre, by configuring the wireless device).
251 lines
7.0 KiB
Lua
251 lines
7.0 KiB
Lua
--[[
|
|
This module implements functions for loading, saving and editing calibre metadata files.
|
|
|
|
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.
|
|
--]]
|
|
|
|
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
|