From 73a6c7b091b4239350da6f0b871a9bfab7fcc80a Mon Sep 17 00:00:00 2001 From: Volterxien Date: Sat, 21 Jun 2025 17:19:52 -0400 Subject: [PATCH] added final functionality --- plugins/opds.koplugin/main.lua | 81 ++++--- plugins/opds.koplugin/opdsbrowser.lua | 295 ++++++++++++++++---------- 2 files changed, 232 insertions(+), 144 deletions(-) diff --git a/plugins/opds.koplugin/main.lua b/plugins/opds.koplugin/main.lua index 5eb6c7cdf..6f08e1672 100644 --- a/plugins/opds.koplugin/main.lua +++ b/plugins/opds.koplugin/main.lua @@ -7,6 +7,7 @@ local InfoMessage = require("ui/widget/infomessage") local LuaSettings = require("luasettings") local NetworkMgr = require("ui/network/manager") local OPDSBrowser = require("opdsbrowser") +local SpinWidget = require("ui/widget/spinwidget") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") @@ -50,12 +51,13 @@ local OPDS = WidgetContainer:extend{ } function OPDS:init() - self.settings = LuaSettings:open(self.opds_settings_file) - if next(self.settings.data) == nil then + self.opds_settings = LuaSettings:open(self.opds_settings_file) + if next(self.opds_settings.data) == nil then self.updated = true -- first run, force flush end - self.servers = self.settings:readSetting("servers", self.default_servers) - self.downloads = self.settings:readSetting("downloads", {}) + self.servers = self.opds_settings:readSetting("servers", self.default_servers) + self.downloads = self.opds_settings:readSetting("downloads", {}) + self.settings = self.opds_settings:readSetting("settings", {}) self:onDispatcherRegisterActions() self.ui.menu:registerToMainMenu(self) end @@ -67,6 +69,7 @@ function OPDS:onDispatcherRegisterActions() end function OPDS:addToMainMenu(menu_items) + --TODO revert changes to original setup if not self.ui.document then -- FileManager menu only menu_items.opds = { text = _("OPDS"), @@ -94,7 +97,9 @@ function OPDS:getOPDSDownloadMenu() text = _("Synchronize now"), callback = function() NetworkMgr:runWhenConnected(function() - self:checkSyncDownload(false) + self.sync = true + self.force = false + self:checkSyncDownload() end) end, }, @@ -107,7 +112,9 @@ function OPDS:getOPDSDownloadMenu() ok_text = "Force sync", ok_callback = function() NetworkMgr:runWhenConnected(function() - self:checkSyncDownload(true) + self.sync = true + self.force = true + self:checkSyncDownload() end) end }) @@ -121,7 +128,7 @@ function OPDS:getOPDSDownloadMenu() end, }, { - text = _("Set OPDS sync directory"), + text = _("Set OPDS sync folder"), callback = function() self:setSyncDir(false) end, @@ -130,53 +137,54 @@ function OPDS:getOPDSDownloadMenu() end function OPDS:setMaxSyncDownload() - local current_max_dl = G_reader_settings:readSetting("opds_sync_max_dl") or 50 - local SpinWidget = require("ui/widget/spinwidget") +-- local current_max_dl = G_reader_settings:readSetting("opds_sync_max_dl") or 50 + local current_max_dl = self.settings.opds_sync_max_dl or 50 local spin = SpinWidget:new{ title_text = "Set maximum sync size", info_text = "Set the max number of books to download at a time", value = current_max_dl, - value_min = 0, - value_max = 999, + value_min = 10, + value_max = 1000, + value_step = 10, + value_hold_step = 50, wrap = true, ok_text = "Save", callback = function(spin) - G_reader_settings:saveSetting("opds_sync_max_dl", spin.value) + self.settings.opds_sync_max_dl = spin.value + self.updated = true +-- G_reader_settings:saveSetting("opds_sync_max_dl", spin.value) end, } UIManager:show(spin) end function OPDS:checkSyncDownload(force) - if not G_reader_settings:readSetting("opds_sync_dir") then +-- if not G_reader_settings:readSetting("opds_sync_dir") then + if not self.settings.opds_sync_dir then UIManager:show(InfoMessage:new{ - text = _("Please select a directory for sync downloads first"), + text = _("Please select a folder for sync downloads first"), }) - self.setSyncDir() + self:setSyncDir() return end + self.sync_opds_browser = OPDSBrowser:new{ + servers = self.servers, + downloads = self.downloads, + settings = self.settings, + sync = self.sync, + sync_force = self.force, + _manager = self, + file_downloaded_callback = function(file) + self:showFileDownloadedDialog(file) + end, + } for _, item in ipairs(self.servers) do if item.sync then - local last_download, new_syncs = OPDSBrowser:syncDownload(item, force, self) + local last_download = self.sync_opds_browser:syncDownload(item, force) if last_download then logger.dbg("Updating opds last download for server " .. item.title) self:updateFieldInCatalog(item, "last_download", last_download) end - if new_syncs then - local function dump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end - end - self:updateFieldInCatalog(item, "pending_syncs", new_syncs) - end end end end @@ -186,7 +194,7 @@ function OPDS:updateFieldInCatalog(item, new_name, new_value) self.updated = true end -function OPDS.setSyncDir() +function OPDS:setSyncDir() local force_chooser_dir if Device:isAndroid() then force_chooser_dir = Device.home_dir @@ -194,8 +202,11 @@ function OPDS.setSyncDir() require("ui/downloadmgr"):new{ onConfirm = function(inbox) - logger.info("set opds sync directory", inbox) - G_reader_settings:saveSetting("opds_sync_dir", inbox) + logger.info("set opds sync folder", inbox) + self.settings.opds_sync_dir = inbox + self.updated = true + logger.dbg(self.settings) +-- G_reader_settings:saveSetting("opds_sync_dir", inbox) end, }:chooseDir(force_chooser_dir) end @@ -249,7 +260,7 @@ end function OPDS:onFlushSettings() if self.updated then - self.settings:flush() + self.opds_settings:flush() self.updated = nil end end diff --git a/plugins/opds.koplugin/opdsbrowser.lua b/plugins/opds.koplugin/opdsbrowser.lua index d53d6c277..bf898557e 100644 --- a/plugins/opds.koplugin/opdsbrowser.lua +++ b/plugins/opds.koplugin/opdsbrowser.lua @@ -12,6 +12,7 @@ local NetworkMgr = require("ui/network/manager") local Notification = require("ui/widget/notification") local OPDSParser = require("opdsparser") local OPDSPSE = require("opdspse") +local TextViewer = require("ui/widget/textviewer") local Trapper = require("ui/trapper") local UIManager = require("ui/uimanager") local http = require("socket.http") @@ -60,6 +61,7 @@ local OPDSBrowser = Menu:extend{ function OPDSBrowser:init() self.item_table = self:genItemTableFromRoot() self.catalog_title = nil + --TODO change behaviour to include sync settings self.title_bar_left_icon = "plus" self.onLeftButtonTap = function() self:addEditCatalog() @@ -76,6 +78,7 @@ local function buildRootEntry(server) password = server.password, raw_names = server.raw_names, -- use server raw filenames for download searchable = server.url and server.url:match("%%s") and true or false, + --TODO add icon for sync sync = server.sync, } end @@ -472,13 +475,16 @@ function OPDSBrowser:genItemTableFromCatalog(catalog, item_url, sync) -- Check for the presence of the pdf suffix and add it -- if it's missing. local href = link.href - if util.getFileNameSuffix(href) ~= "pdf" then - href = href .. ".pdf" + -- Calibre web OPDS download links end with "//" + if not util.stringEndsWith(href, "/pdf/") then + if util.getFileNameSuffix(href) ~= "pdf" then + href = href .. ".pdf" + end + table.insert(item.acquisitions, { + type = link.title, + href = build_href(href), + }) end - table.insert(item.acquisitions, { - type = link.title, - href = build_href(href), - }) end end end @@ -712,7 +718,7 @@ function OPDSBrowser:showDownloads(item) G_reader_settings:saveSetting("download_dir", path) self.download_dialog:setTitle(createTitle(path, filename)) end, - }:chooseDir(self.getCurrentDownloadDir(false)) + }:chooseDir(self:getCurrentDownloadDir()) end, }, { @@ -741,7 +747,7 @@ function OPDSBrowser:showDownloads(item) filename = filename_orig end UIManager:close(dialog) - self.download_dialog:setTitle(createTitle(self.getCurrentDownloadDir(false), filename)) + self.download_dialog:setTitle(createTitle(self:getCurrentDownloadDir(), filename)) end, }, } @@ -777,23 +783,23 @@ function OPDSBrowser:showDownloads(item) }) self.download_dialog = ButtonDialog:new{ - title = createTitle(self.getCurrentDownloadDir(false), filename), + title = createTitle(self:getCurrentDownloadDir(), filename), buttons = buttons, } UIManager:show(self.download_dialog) end -- Returns user selected or last opened folder -function OPDSBrowser.getCurrentDownloadDir(sync) - if sync then - return G_reader_settings:readSetting("opds_sync_dir") +function OPDSBrowser:getCurrentDownloadDir() + if self.sync then + return self.settings.opds_sync_dir else return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir") end end function OPDSBrowser:getLocalDownloadPath(filename, filetype, remote_url, sync) - local download_dir = OPDSBrowser.getCurrentDownloadDir(sync) + local download_dir = self:getCurrentDownloadDir() filename = filename and filename .. "." .. filetype:lower() or self:getServerFileName(remote_url) filename = util.getSafeFilename(filename, download_dir) filename = (download_dir ~= "/" and download_dir or "") .. '/' .. filename @@ -879,6 +885,8 @@ end -- Menu action on item tap (Download a book / Show subcatalog / Search in catalog) function OPDSBrowser:onMenuSelect(item) + --TODO change behaviour to prompt for sync or open + --TODO add sync or open dialog if item.acquisitions and item.acquisitions[1] then -- book logger.dbg("Downloads available:", item) self:showDownloads(item) @@ -1118,114 +1126,190 @@ function OPDSBrowser:showDownloadListItemDialog(item) return true end -function OPDSBrowser:downloadDownloadList(sync, manager) - local info = InfoMessage:new{ text = _("Downloading… (tap to cancel)") } - UIManager:show(info) - UIManager:forceRePaint() +function OPDSBrowser:downloadDownloadList() local dl_list - local function dump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end - end - if sync then + if self.sync then dl_list = self.pending_syncs else - dl_list = self.dowloads + dl_list = self.downloads end - local completed, downloaded = Trapper:dismissableRunInSubprocess(function() - local dl = {} - for _, item in ipairs(dl_list) do - if self:downloadFile(item.file, item.url, item.username, item.password) then - dl[item.file] = true + local function dismissable_download() + local info = InfoMessage:new{ text = _("Downloading… (tap to cancel)") } + UIManager:show(info) + UIManager:forceRePaint() + local completed, downloaded, duplicate_list = Trapper:dismissableRunInSubprocess(function() + local dl = {} + local dupe_list = {} + for _, item in ipairs(dl_list) do + --Maybe add duplicate_list creation for non-sync download list? + if self.sync and not self.sync_force then + if lfs.attributes(item.file) then + table.insert(dupe_list, item) + else + if self:downloadFile(item.file, item.url, item.username, item.password) then + dl[item.file] = true + end + end + else + if self:downloadFile(item.file, item.url, item.username, item.password) then + dl[item.file] = true + end + end end + return dl, dupe_list + end, info) + + if completed then + UIManager:close(info) end - return dl - end, info) - if completed then - UIManager:close(info) - end - local dl_count = #dl_list - for i = dl_count, 1, -1 do - local item = dl_list[i] - if downloaded and downloaded[item.file] then - table.remove(dl_list, i) - else -- if subprocess has been interrupted, check for the downloaded file + local dl_count = 0 + local dl_size = #dl_list + for i = dl_size, 1, -1 do + local item = dl_list[i] + if downloaded and downloaded[item.file] then + dl_count = dl_count + 1 + table.remove(dl_list, i) + else -- if subprocess has been interrupted, check for the downloaded file local attr = lfs.attributes(item.file) - if attr then - if attr.size > 0 then - table.remove(dl_list, i) - else -- incomplete download - os.remove(item.file) + if attr then + if attr.size > 0 then + table.remove(dl_list, i) + if attr.modification > os.time() - 300 then -- Only count files touched in the last 5 mins + dl_count = dl_count + 1 + end + else -- incomplete download + os.remove(item.file) + end end end end - end - dl_count = dl_count - #dl_list - if dl_count > 0 then - if not sync then + local duplicate_count + if duplicate_list then + duplicate_count = #duplicate_list + else + duplicate_count = 0 + end + dl_count = dl_count - duplicate_count + -- Make downloaded count timeout if there's a duplicate file prompt + local timeout = nil + if duplicate_count > 0 then + timeout = 3 + end + if dl_count > 0 then + UIManager:show(InfoMessage:new{ text = T(N_("1 book downloaded", "%1 books downloaded", dl_count), dl_count), timeout = timeout,}) + end + if not self.sync then self:updateDownloadListItemTable() self.download_list_updated = true self._manager.updated = true - UIManager:show(InfoMessage:new{ text = T(N_("1 book downloaded", "%1 books downloaded", dl_count), dl_count) }) else + self.sync_server.pending_syncs = dl_list + self._manager.updated = true end + return duplicate_list + end + + local duplicate_list = dismissable_download() + if #duplicate_list > 0 then + local textviewer + local text = _("These files are already on the device: \n") + for _, entry in ipairs(duplicate_list) do + text = text .. entry.file .. "\n" + end + textviewer = TextViewer:new{ + title = _("Duplicate files"), + text = text, + buttons_table = { + { + { + text = _("Do nothing"), + callback = function() + textviewer:onClose() + self.sync_server.pending_syncs = self.pending_syncs + self._manager.updated = true + end + }, + { + text = _("Overwrite"), + callback = function() + self.sync_force = true + textviewer:onClose() + for _, entry in ipairs(duplicate_list) do + table.insert(dl_list, entry) + end + Trapper:wrap(function() + dismissable_download() + end) + end + }, + { + text = _("Download copies"), + callback = function() + self.sync_force = true + textviewer:onClose() + local copy_download_dir, original_dir, copies_dir, file_name, copy_download_path + copies_dir = "copies" + original_dir, file_name = util.splitFilePathName(duplicate_list[1].file) + copy_download_dir = original_dir .. copies_dir .. "/" + if not util.pathExists(copy_download_dir) then + util.makePath(copy_download_dir) + end + for _, entry in ipairs(duplicate_list) do + original_dir, file_name = util.splitFilePathName(entry.file) + copy_download_path = copy_download_dir .. file_name + entry.file = copy_download_path + table.insert(dl_list, entry) + end + Trapper:wrap(function() + dismissable_download() + end) + end + }, + }, + }, + } + UIManager:show(textviewer) end end -function OPDSBrowser:syncDownload(server, force, manager) +function OPDSBrowser:syncDownload(server) self.root_catalog_password = server.password self.root_catalog_raw_names = server.raw_names self.root_catalog_username = server.username self.root_catalog_title = server.title - self.sync_max_dl = G_reader_settings:readSetting("opds_sync_max_dl") or 50 - self.sync_manager = manager - self.pending_syncs = server.pending_syncs or {} + self.sync_server = server + self.sync_max_dl = self.settings.opds_sync_max_dl or 50 + self.pending_syncs = server.pending_syncs or {} local new_last_download = nil local dl_count = 1 - local function dump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dump(v) .. ',' - end - return s .. '} ' + local sync_list = self:getSyncDownloadList() + if not sync_list then + if #self.pending_syncs > 0 then + Trapper:wrap(function() + self:downloadDownloadList() + end) + return new_last_download else - return tostring(o) + return new_last_download end end - self.sync_list = self:getSyncDownloadList(server.url) - for i, entry in ipairs(self.sync_list) do - print("here") + for i, entry in ipairs(sync_list) do -- First entry in table is the newest -- If already downloaded, return for j, link in ipairs(entry.acquisitions) do - if link.href == server.last_download and not force then - return new_last_download - end -- Only save first link in case of several file types if i == 1 and j == 1 then new_last_download = link.href end -- Don't want kepub + -- Add feature for file types to not sync? if not util.stringEndsWith(link.href, "/kepub/") then local filetype = self.getFiletype(link) - logger.dbg("Filetype for sync download is", filetype) local download_path = self:getLocalDownloadPath(entry.title, filetype, link.href, true) - -- Download only up to max_dl - if dl_count <= self.sync_max_dl then - print(dl_count) + if dl_count <= self.sync_max_dl then -- Append only max_dl entries... may still have sync backlog table.insert(self.pending_syncs, { file = download_path, url = link.href, @@ -1234,19 +1318,13 @@ function OPDSBrowser:syncDownload(server, force, manager) }) dl_count = dl_count + 1 end - --TODO download in background using downloadDownloadList - --TODO implement self.sync_list - --TODO parse more feed --- OPDSBrowser:checkDownloadFile(download_path, link.href, server.username, server.password, nil, true, force) end end end --- Trapper:wrap(function() --- self:downloadDownloadList(true, manager) --- end) --- print(dump(self.syncs)) - print(dump(self.pending_syncs)) - return new_last_download, self.pending_syncs + Trapper:wrap(function() + self:downloadDownloadList() + end) + return new_last_download end function OPDSBrowser.getFiletype(link) @@ -1261,29 +1339,28 @@ function OPDSBrowser.getFiletype(link) end -function OPDSBrowser:getSyncDownloadList(url) +function OPDSBrowser:getSyncDownloadList() local sync_table = {} - local fetch_url = url + local fetch_url = self.sync_server.url local sub_table - local function dump(o) - if type(o) == 'table' then - local s = '{ ' - for k,v in pairs(o) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dump(v) .. ',' - end - return s .. '} ' - else - return tostring(o) - end - end - -- Create table at least as long as max_dl - while #sync_table < self.sync_max_dl do + local up_to_date = false + while #sync_table < self.sync_max_dl and not up_to_date do sub_table = self:genItemTableFromURL(fetch_url, true) - for _, entry in ipairs(sub_table) do - table.insert(sync_table, entry) + if sub_table[1].acquisitions[1].href == self.sync_server.last_download and not self.sync_force then + return nil end - url = sub_table.hrefs.next + for _, entry in ipairs(sub_table) do + if entry.acquisitions[1].href == self.sync_server.last_download and not self.sync_force then + up_to_date = true + break + else + table.insert(sync_table, entry) + end + end + if not sub_table.hrefs.next then + break + end + fetch_url = sub_table.hrefs.next end sync_table.hrefs = sub_table.hrefs return sync_table