Files
koreader/plugins/opds.koplugin/opdsbrowser.lua
Dylan Garrett 26ec5bafe3 [plugin] Display OPDS download titles if available (#8441)
I've found that some OPDS catalogs have multiple downloads of the same filetype, but optimized or formatted in different ways. The Title of the download is much more descriptive in this case, so I thought it would be better to display the title if available.

The OPDS catalog at https://standardebooks.org/opds is a good example. Note how entries in https://standardebooks.org/opds/new-releases have three different epub downloads, titled "Recommended compatible epub", "Advanced epub", and "Kobo Kepub epub".
2021-11-14 07:52:53 +01:00

991 lines
36 KiB
Lua

local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local Cache = require("cache")
local ConfirmBox = require("ui/widget/confirmbox")
local DocumentRegistry = require("document/documentregistry")
local Font = require("ui/font")
local InfoMessage = require("ui/widget/infomessage")
local Menu = require("ui/widget/menu")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local InputDialog = require("ui/widget/inputdialog")
local NetworkMgr = require("ui/network/manager")
local OPDSParser = require("opdsparser")
local Screen = require("device").screen
local UIManager = require("ui/uimanager")
local http = require("socket.http")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local ltn12 = require("ltn12")
local socket = require("socket")
local socketutil = require("socketutil")
local url = require("socket.url")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
-- cache catalog parsed from feed xml
local CatalogCache = Cache:new{
-- Make it 20 slots, with no storage space constraints
slots = 20,
}
local OPDSBrowser = Menu:extend{
opds_servers = G_reader_settings:readSetting("opds_servers", {
{
title = "Project Gutenberg",
url = "https://m.gutenberg.org/ebooks.opds/?format=opds",
},
{
title = "Feedbooks",
url = "https://catalog.feedbooks.com/catalog/public_domain.atom",
},
{
title = "ManyBooks",
url = "http://manybooks.net/opds/index.php",
},
{
title = "Internet Archive",
url = "https://bookserver.archive.org/",
},
{
title = "textos.info (Spanish)",
url = "https://www.textos.info/catalogo.atom",
},
{
title = "Gallica (French)",
url = "https://gallica.bnf.fr/opds",
},
}),
calibre_name = _("Local calibre library"),
calibre_opds = G_reader_settings:readSetting("calibre_opds", {}),
catalog_type = "application/atom%+xml",
search_type = "application/opensearchdescription%+xml",
search_template_type = "application/atom%+xml",
acquisition_rel = "^http://opds%-spec%.org/acquisition",
image_rel = "http://opds-spec.org/image",
thumbnail_rel = "http://opds-spec.org/image/thumbnail",
width = Screen:getWidth(),
height = Screen:getHeight(),
no_title = false,
parent = nil,
}
function OPDSBrowser:init()
self.item_table = self:genItemTableFromRoot()
self.catalog_title = nil
Menu.init(self) -- call parent's init()
end
-- This function is a callback fired from the new
-- catalog dialog, 'addNewCatalog'.
function OPDSBrowser:addServerFromInput(fields)
logger.info("New OPDS catalog input:", fields)
local new_server = {
title = fields[1],
url = (fields[2]:match("^%a+://") and fields[2] or "http://" .. fields[2]),
searchable = (fields[2]:match("%%s") and true or false),
username = fields[3] ~= "" and fields[3] or nil,
-- Allow empty passwords
password = fields[4],
}
table.insert(self.opds_servers, new_server)
self:init()
end
-- This function is a callback fired from the Calibre input
-- dialog 'editCalibreServer'.
function OPDSBrowser:editCalibreFromInput(fields)
logger.dbg("Edit calibre server input:", fields)
if fields[1] then
self.calibre_opds.host = fields[1]
end
if tonumber(fields[2]) then
self.calibre_opds.port = fields[2]
end
if fields[3] and fields[3] ~= "" then
self.calibre_opds.username = fields[3]
else
self.calibre_opds.username = nil
end
if fields[4] then
self.calibre_opds.password = fields[4]
else
self.calibre_opds.password = nil
end
self:init()
end
-- This function shows a dialog with input fields
-- for entering information for an OPDS catalog.
function OPDSBrowser:addNewCatalog()
self.add_server_dialog = MultiInputDialog:new{
title = _("Add OPDS catalog"),
fields = {
{
text = "",
hint = _("Catalog name"),
},
{
text = "",
hint = _("Catalog URL"),
},
{
text = "",
hint = _("Username (optional)"),
},
{
text = "",
hint = _("Password (optional)"),
text_type = "password",
},
},
buttons = {
{
{
text = _("Cancel"),
callback = function()
self.add_server_dialog:onClose()
UIManager:close(self.add_server_dialog)
end
},
{
text = _("Add"),
callback = function()
self.add_server_dialog:onClose()
UIManager:close(self.add_server_dialog)
self:addServerFromInput(MultiInputDialog:getFields())
end
},
},
},
}
UIManager:show(self.add_server_dialog)
self.add_server_dialog:onShowKeyboard()
end
-- This function shows a dialog to the user with input fields
-- for setting Calibre server information.
-- (I think that the Calibre stuff could be moved to a separate file.)
function OPDSBrowser:editCalibreServer()
self.add_server_dialog = MultiInputDialog:new{
title = _("Edit local calibre host and port"),
fields = {
{
--- @todo get IP address of current device
text = self.calibre_opds.host or "192.168.1.1",
hint = _("calibre host"),
},
{
text = self.calibre_opds.port and tostring(self.calibre_opds.port) or "8080",
hint = _("calibre port"),
},
{
text = self.calibre_opds.username or "",
hint = _("Username (optional)"),
},
{
text = self.calibre_opds.password or "",
hint = _("Password (optional)"),
text_type = "password",
},
},
buttons = {
{
{
text = _("Cancel"),
callback = function()
self.add_server_dialog:onClose()
UIManager:close(self.add_server_dialog)
end
},
{
text = _("Apply"),
callback = function()
self.add_server_dialog:onClose()
UIManager:close(self.add_server_dialog)
self:editCalibreFromInput(MultiInputDialog:getFields())
end
},
},
},
}
UIManager:show(self.add_server_dialog)
self.add_server_dialog:onShowKeyboard()
end
-- This function creates the "main menu" for the plugin,
-- wherein the user is shown the default servers, their
-- custom servers, and an item to allow them to add more of their
-- own servers.
function OPDSBrowser:genItemTableFromRoot()
local item_table = {}
-- Loop through the default servers and add them
-- to the item table.
for _, server in ipairs(self.opds_servers) do
table.insert(item_table, {
text = server.title,
content = server.subtitle,
url = server.url,
username = server.username,
password = server.password,
deletable = true,
editable = true,
searchable = server.searchable,
})
end
-- Handle the Calibre server. If it's not set, then place
-- an item that would prompt the user to enter their Calibre settings.
if not self.calibre_opds.host or not self.calibre_opds.port then
-- Here's where we allow the Calibre server to be set.
table.insert(item_table, {
text = self.calibre_name,
callback = function()
self:editCalibreServer()
end,
deletable = false,
})
else
-- Here's where we show the existing Calibre server with
-- the login details stored on the device.
table.insert(item_table, {
text = self.calibre_name,
url = string.format("http://%s:%d/opds",
self.calibre_opds.host, self.calibre_opds.port),
username = self.calibre_opds.username,
password = self.calibre_opds.password,
editable = true,
deletable = false,
searchable = false,
})
end
-- Show the user a list item that would let them add more items
-- to their OPDS server list.
table.insert(item_table, {
text = _("Add new OPDS catalog"),
callback = function()
self:addNewCatalog()
end,
})
return item_table
end
function OPDSBrowser:fetchFeed(item_url, username, password, method)
local sink = {}
socketutil:set_timeout(
socketutil.LARGE_BLOCK_TIMEOUT,
socketutil.LARGE_TOTAL_TIMEOUT
)
-- Prepare the request to send to the server.
local request = {
url = item_url,
method = method and method or "GET",
-- Explicitly specify that we don't support compressed content.
-- Some servers will still break RFC2616 14.3 and send crap instead.
headers = {
["Accept-Encoding"] = "identity",
},
sink = ltn12.sink.table(sink),
user = username,
password = password,
}
logger.info("Request:", request)
-- Fire off the request and wait to see what we get back.
local code, headers = socket.skip(1, http.request(request))
socketutil:reset_timeout()
-- Check the response and raise error message when network is unavailable.
if headers == nil then
error(code)
end
-- Below are numerous if cases to handle different response codes.
if code == 200 then
-- 200 means the request succeeded.
-- If the method sent was HEAD, then we're probably checking for
-- an update and therefore only interested in the last-modified
-- time of the resource (who needs a body when you have a head?).
if method == "HEAD" then
if headers["last-modified"] then
return headers["last-modified"]
else
return
end
end
-- If the method sent was not HEAD, then we are interested in
-- the payload of the request. We'll add that to a table below
-- and return that as the result of this function.
local xml = table.concat(sink)
-- Obviously, check to see if the payload exists.
if xml ~= "" then
return xml
end
elseif method == "HEAD" then
-- Don't show error messages when we check headers only.
return
elseif code == 301 then -- Page has permanently moved
UIManager:show(InfoMessage:new{
text = T(_("The catalog has been permanently moved. Please update catalog URL to '%1'."),
BD.url(headers['Location'])),
})
elseif code == 302
and item_url:match("^https")
and headers.location:match("^http[^s]") then -- Page is redirecting
UIManager:show(InfoMessage:new{
text = T(_("Insecure HTTPS → HTTP downgrade attempted by redirect from:\n\n'%1'\n\nto\n\n'%2'.\n\nPlease inform the server administrator that many clients disallow this because it could be a downgrade attack."),
BD.url(item_url),
BD.url(headers.location)),
icon = "notice-warning",
})
elseif code == 401 then -- Not authorized
UIManager:show(InfoMessage:new{
text = T(_("Authentication required for catalog. Please add a username and password.")),
})
elseif code == 403 then -- Authorization attemp failed
UIManager:show(InfoMessage:new{
text = T(_("Failed to authenticate. Please check your username and password.")),
})
elseif code == 404 then -- Page not found
UIManager:show(InfoMessage:new{
text = T(_("Catalog not found.")),
})
elseif code == 406 then -- Server cannot fulfil our request
UIManager:show(InfoMessage:new{
text = T(_("Cannot get catalog. Server refuses to serve uncompressed content.")),
})
else
-- This block handles all other requests and supplies the user with a generic
-- error message and no more information than the code.
UIManager:show(InfoMessage:new{
text = T(_("Cannot get catalog. Server response code %1."), code),
})
end
end
function OPDSBrowser:parseFeed(item_url, username, password)
local feed_last_modified = self:fetchFeed(item_url, username, password, "HEAD")
local hash = "opds|catalog|" .. item_url
if feed_last_modified then
hash = hash .. "|" .. feed_last_modified
end
local feed = CatalogCache:check(hash)
if feed then
logger.dbg("Cache hit for", hash)
else
logger.dbg("Cache miss for", hash)
feed = self:fetchFeed(item_url, username, password)
if feed then
logger.dbg("Caching", hash)
CatalogCache:insert(hash, feed)
end
end
if feed then
return OPDSParser:parse(feed)
end
end
function OPDSBrowser:getCatalog(item_url, username, password)
local ok, catalog = pcall(self.parseFeed, self, item_url, username, password)
if not ok and catalog then
logger.info("Cannot get catalog info from", item_url or "nil", catalog)
UIManager:show(InfoMessage:new{
text = T(_("Cannot get catalog info from %1"), (item_url and BD.url(item_url) or "nil")),
})
return
end
if ok and catalog then
return catalog
end
end
function OPDSBrowser:genItemTableFromURL(item_url, username, password)
local catalog = self:getCatalog(item_url, username, password)
return self:genItemTableFromCatalog(catalog, item_url, username, password)
end
function OPDSBrowser:getSearchTemplate(osd_url, username, password)
-- parse search descriptor
local search_descriptor = self:parseFeed(osd_url, username, password)
if search_descriptor and search_descriptor.OpenSearchDescription and search_descriptor.OpenSearchDescription.Url then
for _, candidate in ipairs(search_descriptor.OpenSearchDescription.Url) do
if candidate.type and candidate.template and candidate.type:find(self.search_template_type) then
return candidate.template:gsub("{searchTerms}", "%%s")
end
end
end
end
function OPDSBrowser:genItemTableFromCatalog(catalog, item_url, username, password)
local item_table = {}
if not catalog then
return item_table
end
local feed = catalog.feed or catalog
local function build_href(href)
return url.absolute(item_url, href)
end
local hrefs = {}
if feed.link then
for _, link in ipairs(feed.link) do
if link.type ~= nil then
if link.type:find(self.catalog_type) then
if link.rel and link.href then
hrefs[link.rel] = build_href(link.href)
end
end
if link.type:find(self.search_type) then
if link.href then
local stpl = self:getSearchTemplate(build_href(link.href), username, password)
-- The OpenSearchDescription/Url template field might *also* be a relative path...
stpl = build_href(stpl)
-- insert the search item
local item = {}
item.acquisitions = {}
item.text = "Search"
item.callback = function()
self:browseSearchable(stpl, username, password)
end
table.insert(item_table, item)
end
end
end
end
end
item_table.hrefs = hrefs
if username then
item_table.username = username
end
if password then
item_table.password = password
end
if not feed.entry then
if #hrefs == 0 then
UIManager:show(InfoMessage:new{
text = _("Failed to parse the catalog."),
})
end
return item_table
end
for _, entry in ipairs(feed.entry) do
local item = {}
item.acquisitions = {}
if entry.link then
for _, link in ipairs(entry.link) do
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 = build_href(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:match(self.acquisition_rel) then
table.insert(item.acquisitions, {
type = link.type,
href = build_href(link.href),
title = link.title,
})
elseif link.rel == self.thumbnail_rel then
item.thumbnail = build_href(link.href)
elseif link.rel == self.image_rel then
item.image = build_href(link.href)
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
local filetype = util.getFileNameSuffix(link.href)
if filetype ~= "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
if title == "Unknown" then
logger.info("Cannot handle title", entry.title)
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.id = entry.id
item.content = entry.content
item.updated = entry.updated
if username then
item.username = username
end
if password then
item.password = password
end
table.insert(item_table, item)
end
return item_table
end
function OPDSBrowser:updateCatalog(item_url, username, password)
local menu_table = self:genItemTableFromURL(item_url, username, password)
if #menu_table > 0 then
self:switchItemTable(self.catalog_title, menu_table)
if self.page_num <= 1 then
self:onNext()
end
return true
end
end
function OPDSBrowser:appendCatalog(item_url, username, password)
local new_table = self:genItemTableFromURL(item_url, username, password)
if #new_table == 0 then return false end
for _, item in ipairs(new_table) do
table.insert(self.item_table, item)
end
self.item_table.hrefs = new_table.hrefs
self:switchItemTable(self.catalog_title, self.item_table, -1)
return true
end
function OPDSBrowser.getCurrentDownloadDir()
local lastdir = G_reader_settings:readSetting("lastdir")
return G_reader_settings:readSetting("download_dir") or lastdir
end
function OPDSBrowser:downloadFile(item, filetype, remote_url)
-- Download to user selected folder or last opened folder.
local download_dir = self.getCurrentDownloadDir()
local filename = item.title .. "." .. filetype
if item.author then
filename = item.author .. " - " .. filename
end
filename = util.getSafeFilename(filename, download_dir)
local local_path = download_dir .. "/" .. filename
local_path = util.fixUtf8(local_path, "_")
local function download()
UIManager:scheduleIn(1, function()
logger.dbg("Downloading file", local_path, "from", remote_url)
local parsed = url.parse(remote_url)
local code, headers
if parsed.scheme == "http" or parsed.scheme == "https" then
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
code, headers = socket.skip(1, http.request {
url = remote_url,
headers = {
["Accept-Encoding"] = "identity",
},
sink = ltn12.sink.file(io.open(local_path, "w")),
user = item.username,
password = item.password,
})
socketutil:reset_timeout()
else
UIManager:show(InfoMessage:new {
text = T(_("Invalid protocol:\n%1"), parsed.scheme),
timeout = 3,
})
end
if code == 200 then
logger.dbg("File downloaded to", local_path)
if self.file_downloaded_callback then
self.file_downloaded_callback(local_path)
end
elseif code == 302 and remote_url:match("^https") and headers.location:match("^http[^s]") then
util.removeFile(local_path)
UIManager:show(InfoMessage:new{
text = T(_("Insecure HTTPS → HTTP downgrade attempted by redirect from:\n\n'%1'\n\nto\n\n'%2'.\n\nPlease inform the server administrator that many clients disallow this because it could be a downgrade attack."), BD.url(remote_url), BD.url(headers.location)),
icon = "notice-warning",
})
else
util.removeFile(local_path)
UIManager:show(InfoMessage:new {
text = _("Could not save file to:\n") .. BD.filepath(local_path),
timeout = 3,
})
end
end)
UIManager:show(InfoMessage:new{
text = _("Downloading may take several minutes…"),
timeout = 1,
})
end
if lfs.attributes(local_path, "mode") == "file" then
UIManager:show(ConfirmBox:new {
text = T(_("The file %1 already exists. Do you want to overwrite it?"), BD.filepath(local_path)),
ok_text = _("Overwrite"),
ok_callback = function()
download()
end,
})
else
download()
end
end
function OPDSBrowser:createNewDownloadDialog(path, buttons)
self.download_dialog = ButtonDialogTitle:new{
title = T(_("Download folder:\n%1\n\nDownload file type:"), BD.dirpath(path)),
use_info_style = true,
buttons = buttons
}
end
function OPDSBrowser:showDownloads(item)
local acquisitions = item.acquisitions
local downloadsperline = 2
local lines = math.ceil(#acquisitions/downloadsperline)
local buttons = {}
for i = 1, lines do
local line = {}
for j = 1, downloadsperline do
local button = {}
local index = (i-1)*downloadsperline + j
local acquisition = acquisitions[index]
if acquisition then
local filetype = util.getFileNameSuffix(acquisition.href)
logger.dbg("Filetype for download is", filetype)
if not DocumentRegistry:hasProvider("dummy."..filetype) then
filetype = nil
end
if not filetype and DocumentRegistry:hasProvider(nil, acquisition.type) then
filetype = DocumentRegistry:mimeToExt(acquisition.type)
end
if filetype then
filetype = string.lower(filetype)
-- append DOWNWARDS BLACK ARROW ⬇ U+2B07 to format
if acquisition.title then
button.text = acquisition.title .. "\xE2\xAC\x87"
else
button.text = string.upper(filetype) .. "\xE2\xAC\x87"
end
button.callback = function()
self:downloadFile(item, filetype, acquisition.href)
UIManager:close(self.download_dialog)
end
table.insert(line, button)
end
elseif #acquisitions > downloadsperline then
table.insert(line, {text=""})
end
end
table.insert(buttons, line)
end
table.insert(buttons, {})
-- Set download folder and book info buttons.
table.insert(buttons, {
{
text = _("Choose folder"),
callback = function()
require("ui/downloadmgr"):new{
onConfirm = function(path)
logger.info("Download folder set to", path)
G_reader_settings:saveSetting("download_dir", path)
UIManager:nextTick(function()
UIManager:close(self.download_dialog)
self:createNewDownloadDialog(path, buttons)
UIManager:show(self.download_dialog)
end)
end,
}:chooseDir()
end,
},
{
text = _("Book information"),
enabled = type(item.content) == "string",
callback = function()
local TextViewer = require("ui/widget/textviewer")
UIManager:show(TextViewer:new{
title = item.text,
text = util.htmlToPlainTextIfHtml(item.content),
text_face = Font:getFace("x_smallinfofont", G_reader_settings:readSetting("items_font_size")),
})
end,
},
})
self:createNewDownloadDialog(self.getCurrentDownloadDir(), buttons)
UIManager:show(self.download_dialog)
end
function OPDSBrowser:browse(browse_url, username, password)
logger.dbg("Browse OPDS url", browse_url or "nil")
table.insert(self.paths, {
url = browse_url,
username = username,
password = password,
title = self.catalog_title,
})
if not self:updateCatalog(browse_url, username, password) then
table.remove(self.paths)
end
end
function OPDSBrowser:browseSearchable(browse_url, username, password)
self.search_server_dialog = InputDialog:new{
title = _("Search OPDS catalog"),
input = "",
-- @translators: This is an input hint for something to search for in an OPDS catalog, namely a famous author everyone knows. It probably doesn't need to be localized, but this is just here in case another name or book title would be more appropriate outside of a European context.
input_hint = _("Alexandre Dumas"),
input_type = "string",
description = _("%s in url will be replaced by your input"),
buttons = {
{
{
text = _("Cancel"),
callback = function()
UIManager:close(self.search_server_dialog)
end,
},
{
text = _("Search"),
is_enter_default = true,
callback = function()
UIManager:close(self.search_server_dialog)
local search = self.search_server_dialog:getInputText():gsub(" ", "+")
local searched_url = browse_url:gsub("%%s", search)
self:browse(searched_url, username, password)
end,
},
}
},
}
UIManager:show(self.search_server_dialog)
self.search_server_dialog:onShowKeyboard()
end
-- This function is fired when a list item is selected. The function
-- determines what action to performed based on the item's values.
-- Possible actions include: adding a catalog, acquiring a publication,
-- and navigating to another catalog.
function OPDSBrowser:onMenuSelect(item)
logger.dbg("Menu select item", item)
self.catalog_title = self.catalog_title or _("OPDS Catalog")
-- add catalog
if item.callback then
item.callback()
-- acquisition
elseif item.acquisitions and #item.acquisitions > 0 then
logger.dbg("Downloads available:", item)
self:showDownloads(item)
-- navigation
else
self.catalog_title = item.text or self.catalog_title
local connect_callback
if item.searchable then
connect_callback = function()
self:browseSearchable(item.url, item.username, item.password)
end
else
connect_callback = function()
self:browse(item.url, item.username, item.password)
end
end
NetworkMgr:runWhenConnected(connect_callback)
end
return true
end
function OPDSBrowser:editServerFromInput(item, fields)
logger.info("Edit OPDS catalog input:", fields)
for _, server in ipairs(self.opds_servers) do
if server.title == item.text or server.url == item.url then
server.title = fields[1]
server.url = (fields[2]:match("^%a+://") and fields[2] or "http://" .. fields[2])
server.searchable = (fields[2]:match("%%s") and true or false)
server.username = fields[3] ~= "" and fields[3] or nil
server.password = fields[4]
end
end
self:init()
end
function OPDSBrowser:editOPDSServer(item)
logger.info("Edit OPDS Server:", item)
self.edit_server_dialog = MultiInputDialog:new{
title = _("Edit OPDS catalog"),
fields = {
{
text = item.text or "",
hint = _("Catalog name"),
},
{
text = item.url or "",
hint = _("Catalog URL"),
},
{
text = item.username or "",
hint = _("Username (optional)"),
},
{
text = item.password or "",
hint = _("Password (optional)"),
text_type = "password",
},
},
buttons = {
{
{
text = _("Cancel"),
callback = function()
self.edit_server_dialog:onClose()
UIManager:close(self.edit_server_dialog)
end
},
{
text = _("Apply"),
callback = function()
self.edit_server_dialog:onClose()
UIManager:close(self.edit_server_dialog)
self:editServerFromInput(item, MultiInputDialog:getFields())
end
},
},
},
}
UIManager:show(self.edit_server_dialog)
self.edit_server_dialog:onShowKeyboard()
end
function OPDSBrowser:deleteOPDSServer(item)
logger.info("Delete OPDS server:", item)
for i = #self.opds_servers, 1, -1 do
local server = self.opds_servers[i]
if server.title == item.text and server.url == item.url then
table.remove(self.opds_servers, i)
end
end
self:init()
end
function OPDSBrowser:onMenuHold(item)
if item.deletable or item.editable then
self.opds_server_dialog = ButtonDialog:new{
buttons = {
{
{
text = _("Edit"),
enabled = item.editable,
callback = function()
UIManager:close(self.opds_server_dialog)
if item.text ~= self.calibre_name then
self:editOPDSServer(item)
else
self:editCalibreServer(item)
end
end
},
{
text = _("Delete"),
enabled = item.deletable,
callback = function()
UIManager:close(self.opds_server_dialog)
self:deleteOPDSServer(item)
end
},
},
}
}
UIManager:show(self.opds_server_dialog)
return true
end
end
function OPDSBrowser:onReturn()
if #self.paths > 0 then
table.remove(self.paths)
local path = self.paths[#self.paths]
if path then
-- return to last path
self.catalog_title = path.title
self:updateCatalog(path.url, path.username, path.password)
else
-- return to root path, we simply reinit opdsbrowser
self:init()
end
end
return true
end
function OPDSBrowser:onHoldReturn()
if #self.paths > 1 then
local path = self.paths[1]
if path then
for i = #self.paths, 2, -1 do
table.remove(self.paths)
end
self.catalog_title = path.title
self:updateCatalog(path.url, path.username, path.password)
end
end
return true
end
function OPDSBrowser:onNext()
-- self.page_num comes from menu.lua
local page_num = self.page_num
-- fetch more entries until we fill out one page or reach the end
while page_num == self.page_num do
local hrefs = self.item_table.hrefs
if hrefs and hrefs.next then
if not self:appendCatalog(hrefs.next, self.item_table.username, self.item_table.password) then
break -- reach end of paging
end
else
break
end
end
return true
end
return OPDSBrowser