From a71413c03229f4fdff56b06f86ff211a59c4e720 Mon Sep 17 00:00:00 2001 From: Volterxien Date: Sun, 8 Jun 2025 09:29:43 -0400 Subject: [PATCH] add option to sync to opds feed to menu --- plugins/opds.koplugin/main.lua | 90 ++++++++++++- plugins/opds.koplugin/opdsbrowser.lua | 177 +++++++++++++++++++++++--- 2 files changed, 243 insertions(+), 24 deletions(-) diff --git a/plugins/opds.koplugin/main.lua b/plugins/opds.koplugin/main.lua index feaab388d..a1620b462 100644 --- a/plugins/opds.koplugin/main.lua +++ b/plugins/opds.koplugin/main.lua @@ -10,6 +10,9 @@ local util = require("util") local _ = require("gettext") local T = require("ffi/util").template +local logger = require("logger") +local Device = require("device") + local OPDS = WidgetContainer:extend{ name = "opds", opds_settings_file = DataStorage:getSettingsDir() .. "/opds.lua", @@ -64,14 +67,93 @@ end function OPDS:addToMainMenu(menu_items) if not self.ui.document then -- FileManager menu only menu_items.opds = { - text = _("OPDS catalog"), - callback = function() - self:onShowOPDSCatalog() - end, + text = _("OPDS"), + sub_item_table = { + { + text = _("OPDS catalog"), + keep_menu_open = true, + callback = function() + self:onShowOPDSCatalog() + end, + }, + { + text = _("Automatic OPDS download"), + keep_menu_open = true, + sub_item_table = self:getOPDSDownloadMenu(), + }, + }, } end end +function OPDS:getOPDSDownloadMenu() + return { + { + text = _("OPDS sync"), + checked_func = function() + return G_reader_settings:isTrue("opds_sync") + end, + callback = function() + G_reader_settings:toggle("opds_sync") + end, + }, + { + text = _("Perform sync"), + callback = function() + self:checkSyncDownload() + end, + }, + { + text = _("Set OPDS sync directory"), + callback = function() + self:setSyncDir() + end, + }, + } +end + + + +function OPDS:checkSyncDownload() + self.servers = self.settings:readSetting("servers") + 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 + + for i, item in ipairs(self.servers) do + if item.sync then + local table = OPDSBrowser:getSyncDownloadList(item) + print(dump(table)) + end + + end +end + + + +function OPDS:setSyncDir() + local force_chooser_dir + if Device:isAndroid() then + force_chooser_dir = Device.home_dir + end + + require("ui/downloadmgr"):new{ + onConfirm = function(inbox) + logger.info("set opds sync directory", inbox) + G_reader_settings:saveSetting("opds_sync_dir", inbox) + end, + }:chooseDir(force_chooser_dir) +end + function OPDS:onShowOPDSCatalog() self.opds_browser = OPDSBrowser:new{ servers = self.servers, diff --git a/plugins/opds.koplugin/opdsbrowser.lua b/plugins/opds.koplugin/opdsbrowser.lua index a307c6091..f1e70df4a 100644 --- a/plugins/opds.koplugin/opdsbrowser.lua +++ b/plugins/opds.koplugin/opdsbrowser.lua @@ -68,13 +68,14 @@ end local function buildRootEntry(server) return { - text = server.title, - mandatory = server.username and "\u{f2c0}", - url = server.url, - username = server.username, - 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, + text = server.title, + mandatory = server.username and "\u{f2c0}", + url = server.url, + username = server.username, + 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, + sync = server.sync, } end @@ -138,6 +139,7 @@ function OPDSBrowser:addEditCatalog(item) callback = function() local new_fields = dialog:getFields() new_fields[5] = check_button_raw_names.checked or nil + new_fields[6] = check_button_sync_catalog.checked or nil self:editCatalogFromInput(new_fields, item) UIManager:close(dialog) end, @@ -150,7 +152,13 @@ function OPDSBrowser:addEditCatalog(item) checked = item and item.raw_names, parent = dialog, } + check_button_sync_catalog = CheckButton:new{ + text = _("Sync catalog"), + checked = item and item.sync, + parent = dialog, + } dialog:addWidget(check_button_raw_names) + dialog:addWidget(check_button_sync_catalog) UIManager:show(dialog) dialog:onShowKeyboard() end @@ -193,11 +201,12 @@ end -- Saves catalog properties from input dialog function OPDSBrowser:editCatalogFromInput(fields, item, no_refresh) local new_server = { - title = fields[1], - url = fields[2]:match("^%a+://") and fields[2] or "http://" .. fields[2], - username = fields[3] ~= "" and fields[3] or nil, - password = fields[4] ~= "" and fields[4] or nil, - raw_names = fields[5], + title = fields[1], + url = fields[2]:match("^%a+://") and fields[2] or "http://" .. fields[2], + username = fields[3] ~= "" and fields[3] or nil, + password = fields[4] ~= "" and fields[4] or nil, + raw_names = fields[5], + sync = fields[6], } local new_item = buildRootEntry(new_server) local new_idx, itemnumber @@ -341,10 +350,10 @@ function OPDSBrowser:genItemTableFromURL(item_url) }) catalog = nil end - return self:genItemTableFromCatalog(catalog, item_url) + return self:genItemTableFromCatalog(catalog, item_url, true) end -function OPDSBrowser:genItemTableFromCatalog(catalog, item_url) +function OPDSBrowser:genItemTableFromCatalog(catalog, item_url, sync) local item_table = {} if not catalog then return item_table @@ -649,13 +658,13 @@ function OPDSBrowser:showDownloads(item) text = text .. "\u{2B07}", -- append DOWNWARDS BLACK ARROW callback = function() UIManager:close(self.download_dialog) - local local_path = self:getLocalDownloadPath(filename, filetype, acquisition.href) + local local_path = self:getLocalDownloadPath(filename, filetype, acquisition.href, false) self:checkDownloadFile(local_path, acquisition.href, self.root_catalog_username, self.root_catalog_password, self.file_downloaded_callback) end, hold_callback = function() UIManager:close(self.download_dialog) table.insert(self.downloads, { - file = self:getLocalDownloadPath(filename, filetype, acquisition.href), + file = self:getLocalDownloadPath(filename, filetype, acquisition.href, false), url = acquisition.href, info = type(item.content) == "string" and util.htmlToPlainTextIfHtml(item.content) or "", catalog = self.root_catalog_title, @@ -772,12 +781,16 @@ function OPDSBrowser:showDownloads(item) end -- Returns user selected or last opened folder -function OPDSBrowser.getCurrentDownloadDir() - return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir") +function OPDSBrowser.getCurrentDownloadDir(sync) + if sync then + return G_reader_settings:readSetting("opds_sync_dir") or G_reader_settings:readSetting("lastdir") + else + return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir") + end end -function OPDSBrowser:getLocalDownloadPath(filename, filetype, remote_url) - local download_dir = OPDSBrowser.getCurrentDownloadDir() +function OPDSBrowser:getLocalDownloadPath(filename, filetype, remote_url, sync) + local download_dir = OPDSBrowser.getCurrentDownloadDir(sync) 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 @@ -809,6 +822,7 @@ function OPDSBrowser:checkDownloadFile(local_path, remote_url, username, passwor end function OPDSBrowser:downloadFile(local_path, remote_url, username, password, caller_callback) + logger.dbg("Downloading file", local_path, "from", remote_url) local code, headers, status local parsed = url.parse(remote_url) @@ -1137,4 +1151,127 @@ function OPDSBrowser:downloadDownloadList() end end +function OPDSBrowser:getSyncDownloadList(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 + local ok, catalog = pcall(self.parseFeed, self, server.url) + local item_table = {} + local feed = catalog.feed or catalog + local function build_href(href) + return url.absolute(item_url, href) + end + for __, entry in ipairs(feed.entry or {}) do + local item = {} + item.acquisitions = {} + if entry.link then + for ___, link in ipairs(entry.link) do + local link_href = build_href(link.href) + if link.type and link.type:find(self.catalog_type) + and (not link.rel + or link.rel == "subsection" + or link.rel == "http://opds-spec.org/subsection" + or link.rel == "http://opds-spec.org/sort/popular" + or link.rel == "http://opds-spec.org/sort/new") then + item.url = link_href + end + -- Some catalogs do not use the rel attribute to denote + -- a publication. Arxiv uses title. Specifically, it uses + -- a title attribute that contains pdf. (title="pdf") + if link.rel or link.title then + if link.rel == self.borrow_rel then + table.insert(item.acquisitions, { + type = "borrow", + }) + elseif link.rel and link.rel:match(self.acquisition_rel) then + table.insert(item.acquisitions, { + type = link.type, + href = link_href, + title = link.title, + }) + elseif link.rel == self.stream_rel then + -- https://vaemendis.net/opds-pse/ + -- «count» MUST provide the number of pages of the document + -- namespace may be not "pse" + local count, last_read + for k, v in pairs(link) do + if k:sub(-6) == ":count" then + count = tonumber(v) + elseif k:sub(-9) == ":lastRead" then + last_read = tonumber(v) + end + end + if count then + table.insert(item.acquisitions, { + type = link.type, + href = link_href, + title = link.title, + count = count, + last_read = last_read and last_read > 0 and last_read or nil + }) + end + elseif self.thumbnail_rel[link.rel] then + item.thumbnail = link_href + elseif self.image_rel[link.rel] then + item.image = link_href + elseif link.rel ~= "alternate" and DocumentRegistry:hasProvider(nil, link.type) then + table.insert(item.acquisitions, { + type = link.type, + href = link_href, + title = link.title, + }) + end + -- This statement grabs the catalog items that are + -- indicated by title="pdf" or whose type is + -- "application/pdf" + if link.title == "pdf" or link.type == "application/pdf" + and link.rel ~= "subsection" then + -- 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" + end + table.insert(item.acquisitions, { + type = link.title, + href = build_href(href), + }) + end + end + end + end + local title = _("Unknown") + if type(entry.title) == "string" then + title = entry.title + elseif type(entry.title) == "table" then + if type(entry.title.type) == "string" and entry.title.div ~= "" then + title = entry.title.div + end + end + item.text = title + local author = _("Unknown Author") + if type(entry.author) == "table" and entry.author.name then + author = entry.author.name + if type(author) == "table" then + if #author > 0 then + author = table.concat(author, ", ") + else + -- we may get an empty table on https://gallica.bnf.fr/opds + author = nil + end + end + if author then + item.text = title .. " - " .. author + end + end + item.title = title + item.author = author + item.content = entry.content or entry.summary + table.insert(item_table, item) + end + return item_table +end + + return OPDSBrowser