Initial commit
This commit is contained in:
parent
bba2b611c2
commit
3d051a93cc
2 changed files with 372 additions and 0 deletions
6
kosynccloud.koplugin/_meta.lua
Normal file
6
kosynccloud.koplugin/_meta.lua
Normal file
|
@ -0,0 +1,6 @@
|
|||
local _ = require("gettext")
|
||||
return {
|
||||
name = "kosynccloud",
|
||||
fullname = _("Progress Cloudsync"),
|
||||
description = _([[Synchronizes your reading progress across your KOReader devices.]]),
|
||||
}
|
366
kosynccloud.koplugin/main.lua
Normal file
366
kosynccloud.koplugin/main.lua
Normal file
|
@ -0,0 +1,366 @@
|
|||
local BD = require("ui/bidi")
|
||||
local ButtonDialog = require("ui/widget/buttondialog")
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local DataStorage = require("datastorage")
|
||||
local Device = require("device")
|
||||
local Dispatcher = require("dispatcher")
|
||||
local Event = require("ui/event")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local Math = require("optmath")
|
||||
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local SQ3 = require("lua-ljsqlite3/init")
|
||||
local SyncService = require("frontend/apps/cloudstorage/syncservice")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
local md5 = require("ffi/sha2").md5
|
||||
local random = require("random")
|
||||
local time = require("ui/time")
|
||||
local util = require("util")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
-- Current DB schema version
|
||||
local DB_SCHEMA_VERSION = 20250120
|
||||
local db_location = DataStorage:getSettingsDir() .. "/kosynccloud.sqlite3"
|
||||
|
||||
if G_reader_settings:hasNot("device_id") then
|
||||
G_reader_settings:saveSetting("device_id", random.uuid())
|
||||
end
|
||||
|
||||
local KOSyncCloud = WidgetContainer:extend {
|
||||
name = "kosynccloud",
|
||||
--is_doc = false,
|
||||
|
||||
last_page = nil,
|
||||
|
||||
is_enabled = nil,
|
||||
|
||||
}
|
||||
|
||||
KOSyncCloud.default_settings = {
|
||||
is_enabled = true,
|
||||
}
|
||||
|
||||
function KOSyncCloud:init()
|
||||
self.last_page = -1
|
||||
self.device_id = G_reader_settings:readSetting("device_id")
|
||||
|
||||
self.settings = G_reader_settings:readSetting("kosynccloud", self.default_settings)
|
||||
|
||||
self:checkInitDatabase()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
|
||||
function KOSyncCloud:addToMainMenu(menu_items)
|
||||
menu_items.kosynccloud = {
|
||||
text = _("Reading Progress Cloud"),
|
||||
sorting_hint = "more_tools",
|
||||
{
|
||||
text = _("Cloud sync settings"),
|
||||
callback = function(touchmenu_instance)
|
||||
local server = self.settings.sync_server
|
||||
local edit_cb = function()
|
||||
local sync_settings = SyncService:new {}
|
||||
sync_settings.onClose = function(this)
|
||||
UIManager:close(this)
|
||||
end
|
||||
sync_settings.onConfirm = function(sv)
|
||||
if server and (server.type ~= sv.type
|
||||
or server.url ~= sv.url
|
||||
or server.address ~= sv.address) then
|
||||
SyncService.removeLastSyncDB(db_location)
|
||||
end
|
||||
self.settings.sync_server = sv
|
||||
touchmenu_instance:updateItems()
|
||||
end
|
||||
UIManager:show(sync_settings)
|
||||
end
|
||||
if not server then
|
||||
edit_cb()
|
||||
return
|
||||
end
|
||||
local dialogue
|
||||
local delete_button = {
|
||||
text = _("Delete"),
|
||||
callback = function()
|
||||
UIManager:close(dialogue)
|
||||
UIManager:show(ConfirmBox:new {
|
||||
text = _("Delete server info?"),
|
||||
cancel_text = _("Cancel"),
|
||||
cancel_callback = function()
|
||||
return
|
||||
end,
|
||||
ok_text = _("Delete"),
|
||||
ok_callback = function()
|
||||
self.settings.sync_server = nil
|
||||
SyncService.removeLastSyncDB(db_location)
|
||||
touchmenu_instance:updateItems()
|
||||
end,
|
||||
})
|
||||
end,
|
||||
}
|
||||
local edit_button = {
|
||||
text = _("Edit"),
|
||||
callback = function()
|
||||
UIManager:close(dialogue)
|
||||
edit_cb()
|
||||
end
|
||||
}
|
||||
local close_button = {
|
||||
text = _("Close"),
|
||||
callback = function()
|
||||
UIManager:close(dialogue)
|
||||
end
|
||||
}
|
||||
local type = server.type == "dropbox" and " (DropBox)" or " (WebDAV)"
|
||||
dialogue = ButtonDialog:new {
|
||||
title = T(_("Cloud storage:\n%1\n\nFolder path:\n%2\n\nSet up the same cloud folder on each device to sync across your devices."),
|
||||
server.name .. " " .. type, SyncService.getReadablePath(server)),
|
||||
buttons = {
|
||||
{ delete_button, edit_button, close_button }
|
||||
},
|
||||
}
|
||||
UIManager:show(dialogue)
|
||||
end,
|
||||
enabled_func = function() return self.settings.is_enabled end,
|
||||
keep_menu_open = true,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Synchronize now"),
|
||||
callback = function()
|
||||
self:onSyncProgress()
|
||||
end,
|
||||
enabled_func = function()
|
||||
return self:canSync()
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
separator = true,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function KOSyncCloud:canSync()
|
||||
return self.settings.sync_server ~= nil and self.settings.is_enabled
|
||||
end
|
||||
|
||||
function KOSyncCloud:checkInitDatabase()
|
||||
local conn = SQ3.open(db_location)
|
||||
|
||||
if not conn:exec("PRAGMA table_info('progresses');") then
|
||||
self:createDB(conn)
|
||||
end
|
||||
|
||||
conn:close()
|
||||
end
|
||||
|
||||
function KOSyncCloud:createDB(conn)
|
||||
-- Make it WAL, if possible
|
||||
if Device:canUseWAL() then
|
||||
conn:exec("PRAGMA journal_mode=WAL;")
|
||||
else
|
||||
conn:exec("PRAGMA journal_mode=TRUNCATE;")
|
||||
end
|
||||
|
||||
local sql_stmt = [[
|
||||
CREATE TABLE IF NOT EXISTS progresses
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document TEXT UNIQUE,
|
||||
progress TEXT,
|
||||
percentage REAL,
|
||||
device TEXT,
|
||||
device_id TEXT,
|
||||
timestamp INTEGER
|
||||
);
|
||||
]]
|
||||
conn:exec(sql_stmt)
|
||||
|
||||
-- DB schema version
|
||||
conn:exec(string.format("PRAGMA user_version=%d;", DB_SCHEMA_VERSION))
|
||||
end
|
||||
|
||||
function KOSyncCloud:getLastPercent()
|
||||
if self.ui.document.info.has_pages then
|
||||
return Math.roundPercent(self.ui.paging:getLastPercent())
|
||||
else
|
||||
return Math.roundPercent(self.ui.rolling:getLastPercent())
|
||||
end
|
||||
end
|
||||
|
||||
function KOSyncCloud:getLastProgress()
|
||||
if self.ui.document.info.has_pages then
|
||||
return self.ui.paging:getLastProgress()
|
||||
else
|
||||
return self.ui.rolling:getLastProgress()
|
||||
end
|
||||
end
|
||||
|
||||
function KOSyncCloud:getDocumentDigest()
|
||||
return self.ui.doc_settings:readSetting("partial_md5_checksum")
|
||||
end
|
||||
|
||||
function KOSyncCloud:getDocValuesFromDb()
|
||||
local doc_digest = self:getDocumentDigest()
|
||||
|
||||
local conn = SQ3.open(db_location)
|
||||
|
||||
local stmt = conn:prepare("SELECT 1 FROM progresses WHERE document = ?;")
|
||||
local result = stmt:reset():bind(doc_digest):step()
|
||||
|
||||
local progress, percentage, device_id = nil, nil, nil
|
||||
|
||||
if result ~= nil then
|
||||
local sql_stmt = "SELECT progress, percentage, device_id FROM progresses WHERE document = '%s';"
|
||||
progress, percentage, device_id = conn:rowexec(string.format(sql_stmt, doc_digest))
|
||||
end
|
||||
|
||||
logger.dbg(progress, percentage, device_id)
|
||||
conn:close()
|
||||
|
||||
return progress, percentage, device_id
|
||||
end
|
||||
|
||||
function KOSyncCloud:showSyncedMessage()
|
||||
UIManager:show(InfoMessage:new {
|
||||
text = _("Progress has been synchronized."),
|
||||
timeout = 3,
|
||||
})
|
||||
end
|
||||
|
||||
function KOSyncCloud:syncToProgress()
|
||||
local progress, percentage, device_id = self:getDocValuesFromDb()
|
||||
if device_id ~= nil and device_id ~= self.device_id then
|
||||
if percentage ~= self:getLastPercent() then
|
||||
logger.dbg("KOSyncCloud: [Sync] progress to", progress)
|
||||
if self.ui.document.info.has_pages then
|
||||
self.ui:handleEvent(Event:new("GotoPage", tonumber(progress)))
|
||||
else
|
||||
self.ui:handleEvent(Event:new("GotoXPointer", progress))
|
||||
end
|
||||
self:showSyncedMessage()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function KOSyncCloud:onReaderReady()
|
||||
UIManager:nextTick(function()
|
||||
self:syncToProgress()
|
||||
end)
|
||||
|
||||
self:registerEvents()
|
||||
|
||||
self.last_page = self.ui:getCurrentPage()
|
||||
end
|
||||
|
||||
function KOSyncCloud.onSync(local_path, cached_path, income_path)
|
||||
-- TODO: Implement sync logic
|
||||
local conn_income = SQ3.open(income_path)
|
||||
local ok1, v1 = pcall(conn_income.rowexec, conn_income, "PRAGMA schema_version")
|
||||
if not ok1 or tonumber(v1) == 0 then
|
||||
-- no income db or wrong db, first time sync
|
||||
logger.warn("Opening progress income DB failed", v1)
|
||||
return true
|
||||
end
|
||||
|
||||
local sql = "attach '" .. income_path:gsub("'", "''") .. "' as income_db;"
|
||||
|
||||
local conn_cached = SQ3.open(cached_path)
|
||||
local ok2, v2 = pcall(conn_cached.rowexec, conn_cached, "PRAGMA schema_version")
|
||||
local attached_cache
|
||||
if not ok2 or tonumber(v2) == 0 then
|
||||
-- no cached or error, no item to delete
|
||||
logger.warn("Opening progress cached DB failed", v2)
|
||||
else
|
||||
attached_cache = true
|
||||
sql = sql .. "attach '" .. cached_path:gsub("'", "''") .. [[' as cached_db;
|
||||
]]
|
||||
-- Skip this part for now
|
||||
end
|
||||
conn_cached:close()
|
||||
conn_income:close()
|
||||
|
||||
local conn = SQ3.open(local_path)
|
||||
local ok3, v3 = pcall(conn.exec, conn, "PRAGMA schema_version")
|
||||
if not ok3 or tonumber(v3) == 0 then
|
||||
-- no local db, this is an error
|
||||
logger.err("progress open local DB", v3)
|
||||
return false
|
||||
end
|
||||
|
||||
sql = sql .. [[
|
||||
UPDATE progresses AS p
|
||||
SET progress = i.progress,
|
||||
percentage = i.percentage,
|
||||
device = i.device,
|
||||
device_id = i.device_id,
|
||||
timestamp = i.timestamp
|
||||
FROM income_db.progresses AS i
|
||||
WHERE p.document = i.document AND p.timestamp < i.timestamp;
|
||||
|
||||
INSERT INTO progresses (
|
||||
document, progress, percentage, device, device_id, timestamp
|
||||
) SELECT
|
||||
document, progress, percentage, device, device_id, timestamp
|
||||
FROM income_db.progresses
|
||||
WHERE document NOT IN (SELECT document FROM progresses);
|
||||
]]
|
||||
|
||||
conn:exec(sql)
|
||||
pcall(conn.exec, conn, "COMMIT;")
|
||||
conn:exec("DETACH income_db;" .. (attached_cache and "DETACH cached_db;" or ""))
|
||||
conn:close()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function KOSyncCloud:onSyncProgress()
|
||||
if not self:canSync() then return end
|
||||
|
||||
UIManager:show(InfoMessage:new {
|
||||
text = _("Syncing reading progress. This may take a while."),
|
||||
timeout = 1,
|
||||
})
|
||||
|
||||
UIManager:nextTick(function()
|
||||
SyncService.sync(self.settings.sync_server, db_location, self.onSync)
|
||||
end)
|
||||
end
|
||||
|
||||
function KOSyncCloud:registerEvents()
|
||||
self.onPageUpdate = self._onPageUpdate
|
||||
end
|
||||
|
||||
function KOSyncCloud:_onPageUpdate(page)
|
||||
if page == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if self.last_page ~= page then
|
||||
self.last_page = page
|
||||
self:updateProgress()
|
||||
end
|
||||
end
|
||||
|
||||
function KOSyncCloud:updateProgress()
|
||||
local doc_digest = self:getDocumentDigest()
|
||||
local progress = self:getLastProgress()
|
||||
local percentage = self:getLastPercent()
|
||||
|
||||
local conn = SQ3.open(db_location)
|
||||
local stmt = conn:prepare("SELECT 1 FROM progresses WHERE document = ?;")
|
||||
local result = stmt:reset():bind(doc_digest):step()
|
||||
if result == nil then
|
||||
stmt = conn:prepare("INSERT INTO progresses VALUES (NULL, ?, ?, ?, ?, ?, (SELECT unixepoch()));")
|
||||
stmt:reset():bind(doc_digest, progress, percentage, Device.model, self.device_id):step()
|
||||
else
|
||||
stmt = conn:prepare(
|
||||
"UPDATE progresses SET progress = ?, percentage = ?, device = ?, device_id = ?, timestamp = (SELECT unixepoch()) WHERE document = ?;")
|
||||
stmt:reset():bind(progress, percentage, Device.model, self.device_id, doc_digest):step()
|
||||
end
|
||||
conn:close()
|
||||
end
|
||||
|
||||
return KOSyncCloud
|
Loading…
Add table
Reference in a new issue