Skip to content

Instantly share code, notes, and snippets.

@mallomar
Last active September 24, 2025 16:24
Show Gist options
  • Select an option

  • Save mallomar/56c524d17bcbc026288ffadc9ff8d174 to your computer and use it in GitHub Desktop.

Select an option

Save mallomar/56c524d17bcbc026288ffadc9ff8d174 to your computer and use it in GitHub Desktop.
Adds a Menu Item in Tools Showing the Book's Partial Sync Hash
--[[
KOReader Patch: Add Book Info with Both Hashes to Menu
Adds a menu item to display the book's sync hash and Readest meta hash
--]]
-- Function to find the KOReader sync hash
local function findSyncHash(file_path)
if not file_path then return nil end
local ok_ds, DocSettings = pcall(require, "docsettings")
if ok_ds then
local doc_settings = DocSettings:open(file_path)
if doc_settings and doc_settings.data and doc_settings.data.partial_md5_checksum then
return doc_settings.data.partial_md5_checksum
end
end
return nil
end
-- Function to get Readest meta hash (using same logic as the plugin)
local function getReadestMetaHash(file_path)
if not file_path then return nil end
local ok_ds, DocSettings = pcall(require, "docsettings")
if not ok_ds then return nil end
local doc_settings = DocSettings:open(file_path)
if not doc_settings then return nil end
-- Check if meta_hash is already cached
local readest_sync = doc_settings:readSetting("readest_sync") or {}
if readest_sync.meta_hash then
return readest_sync.meta_hash
end
-- Calculate meta_hash using the same logic as Readest plugin
local util = require("util")
local sha2 = require("ffi/sha2")
local doc_props = doc_settings:readSetting("doc_props") or {}
local title = doc_props.title or ''
if title == '' then
local doc_path = doc_settings:readSetting("doc_path") or ''
local doc_path_part, filename = util.splitFilePathName(doc_path)
local basename, suffix = util.splitFileNameSuffix(filename)
title = basename or ''
end
local function normalizeAuthor(author)
return author:gsub("^%s*(.-)%s*$", "%1")
end
local function normalizeIdentifier(identifier)
if identifier:match("urn:") then
return identifier:match("([^:]+)$")
elseif identifier:match(":") then
return identifier:match("^[^:]+:(.+)$")
end
return identifier
end
local authors = doc_props.authors or ''
if authors:find("\n") then
local author_array = util.splitToArray(authors, "\n")
for i, author in ipairs(author_array) do
author_array[i] = normalizeAuthor(author)
end
authors = table.concat(author_array, ",")
else
authors = normalizeAuthor(authors)
end
local identifiers = doc_props.identifiers or ''
if identifiers:find("\n") then
local id_array = util.splitToArray(identifiers, "\n")
for i, id in ipairs(id_array) do
id_array[i] = normalizeIdentifier(id)
end
identifiers = table.concat(id_array, ",")
else
identifiers = normalizeIdentifier(identifiers)
end
local doc_meta = title .. "|" .. authors .. "|" .. identifiers
local meta_hash = sha2.md5(doc_meta)
return meta_hash
end
-- Function to find book metadata
local function findBookMetadata(file_path)
if not file_path then return nil, nil, nil end
local title, author, isbn = nil, nil, nil
-- Try to get metadata from DocSettings first (most reliable)
local ok_ds, DocSettings = pcall(require, "docsettings")
if ok_ds then
local doc_settings = DocSettings:open(file_path)
if doc_settings and doc_settings.data and doc_settings.data.doc_props then
local props = doc_settings.data.doc_props
title = props.title
author = props.authors
isbn = props.identifiers
end
end
-- Try to get metadata from live document if available
local ok_reader, ReaderUI = pcall(require, "apps/reader/readerui")
if ok_reader and ReaderUI.instance and ReaderUI.instance.document then
local document = ReaderUI.instance.document
if document.getMetadata then
local ok, metadata = pcall(document.getMetadata, document)
if ok and metadata then
title = title or metadata.title
author = author or metadata.author or metadata.creator
isbn = isbn or metadata.identifier or metadata.isbn or metadata.ISBN
end
end
if document.getProps then
local ok, props = pcall(document.getProps, document)
if ok and props then
title = title or props.title
author = author or props.author or props.creator
isbn = isbn or props.identifier or props.isbn or props.ISBN
end
end
end
return title, author, isbn
end
-- Function to show comprehensive book info dialog
local function showBookInfoDialog()
local file_path = nil
local ok_reader, ReaderUI = pcall(require, "apps/reader/readerui")
if ok_reader and ReaderUI.instance and ReaderUI.instance.document then
file_path = ReaderUI.instance.document.file
end
local sync_hash = findSyncHash(file_path)
local meta_hash = getReadestMetaHash(file_path)
local title, author, isbn = findBookMetadata(file_path)
local sync_hash_text = sync_hash or "Sync hash not found"
local meta_hash_text = meta_hash or "Meta hash not available"
local isbn_text = isbn or "ISBN not found"
local title_text = title or "Title not found"
local author_text = author or "Author not found"
local filename = file_path and string.match(file_path, "[^/]+$") or "unknown"
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local dialog_text = string.format(
"Book Information:\n\n" ..
"KOReader Sync Hash:\n%s\n\n" ..
"Readest Meta Hash:\n%s\n\n" ..
"Title: %s\n" ..
"Author: %s\n" ..
"ISBN: %s\n\n" ..
"The sync hash is KOReader's book identifier.\n" ..
"The meta hash is calculated from title|author|identifiers.\n\n" ..
"File: %s",
sync_hash_text, meta_hash_text, title_text, author_text, isbn_text, filename
)
local info = InfoMessage:new{
text = dialog_text,
-- Remove timeout to make dialog persistent
}
UIManager:show(info)
end
-- Create the menu item
local function createBookInfoMenuItem()
return {
text = "Show Book Info & Hashes",
callback = function()
showBookInfoDialog()
end,
}
end
-- Add menu item to the specified section
local function patchMenu(menu, order)
if order and order.more_tools then
table.insert(order.more_tools, "book_info_hashes")
menu.menu_items.book_info_hashes = createBookInfoMenuItem()
elseif order and order.tools then
table.insert(order.tools, "book_info_hashes")
menu.menu_items.book_info_hashes = createBookInfoMenuItem()
else
menu.menu_items.book_info_hashes = createBookInfoMenuItem()
end
end
-- Hook into ReaderMenu
local ok_reader_menu, ReaderMenu = pcall(require, "apps/reader/modules/readermenu")
if ok_reader_menu then
local orig_ReaderMenu_setUpdateItemTable = ReaderMenu.setUpdateItemTable
ReaderMenu.setUpdateItemTable = function(self)
local ok_order, reader_order = pcall(require, "ui/elements/reader_menu_order")
if ok_order then
patchMenu(self, reader_order)
end
orig_ReaderMenu_setUpdateItemTable(self)
end
end
-- Hook into FileManagerMenu
local ok_fm_menu, FileManagerMenu = pcall(require, "apps/filemanager/filemanagermenu")
if ok_fm_menu then
local orig_FileManagerMenu_setUpdateItemTable = FileManagerMenu.setUpdateItemTable
FileManagerMenu.setUpdateItemTable = function(self)
local ok_order, fm_order = pcall(require, "ui/elements/filemanager_menu_order")
if ok_order then
patchMenu(self, fm_order)
end
orig_FileManagerMenu_setUpdateItemTable(self)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment