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