mirror of
https://github.com/koreader/koreader
synced 2024-11-08 07:10:27 +00:00
924 lines
34 KiB
Lua
924 lines
34 KiB
Lua
local BD = require("ui/bidi")
|
|
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 InputDialog = require("ui/widget/inputdialog")
|
|
local Menu = require("ui/widget/menu")
|
|
local MultiInputDialog = require("ui/widget/multiinputdialog")
|
|
local NetworkMgr = require("ui/network/manager")
|
|
local OPDSParser = require("opdsparser")
|
|
local OPDSPSE = require("opdspse")
|
|
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 = "Standard Ebooks",
|
|
url = "https://standardebooks.org/feeds/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",
|
|
stream_rel = "http://vaemendis.net/opds-pse/stream",
|
|
|
|
root_catalog_title = nil,
|
|
root_catalog_username = nil,
|
|
root_catalog_password = nil,
|
|
|
|
width = Screen:getWidth(),
|
|
height = Screen:getHeight(),
|
|
title_shrink_font_to_fit = true,
|
|
}
|
|
|
|
function OPDSBrowser:init()
|
|
self.item_table = self:genItemTableFromRoot()
|
|
self.catalog_title = nil
|
|
self.title_bar_left_icon = "plus"
|
|
self.onLeftButtonTap = function()
|
|
self:addEditCatalog()
|
|
end
|
|
Menu.init(self) -- call parent's init()
|
|
end
|
|
|
|
-- Builds the root list of catalogs
|
|
function OPDSBrowser:genItemTableFromRoot()
|
|
local item_table = {
|
|
{ -- calibre is the first and non-deletable item
|
|
text = self.calibre_name,
|
|
url = self.calibre_opds.host and self.calibre_opds.port and
|
|
string.format("http://%s:%d/opds", self.calibre_opds.host, self.calibre_opds.port),
|
|
username = self.calibre_opds.username,
|
|
password = self.calibre_opds.password,
|
|
searchable = false,
|
|
},
|
|
}
|
|
for _, server in ipairs(self.opds_servers) do
|
|
table.insert(item_table, {
|
|
text = server.title,
|
|
url = server.url,
|
|
username = server.username,
|
|
password = server.password,
|
|
searchable = server.url:match("%%s") and true or false,
|
|
})
|
|
end
|
|
return item_table
|
|
end
|
|
|
|
-- Shows dialog to edit properties of the new/existing catalog
|
|
function OPDSBrowser:addEditCatalog(item, is_calibre)
|
|
local title
|
|
local fields = {{}, {}, {}, {}}
|
|
if is_calibre then
|
|
title = _("Edit local calibre host and port")
|
|
fields[1].text = self.calibre_opds.host or "192.168.1.1"
|
|
fields[1].hint = _("calibre host")
|
|
fields[2].text = self.calibre_opds.port and tostring(self.calibre_opds.port) or "8080"
|
|
fields[2].hint = _("calibre port")
|
|
fields[3].text = self.calibre_opds.username
|
|
fields[4].text = self.calibre_opds.password
|
|
else
|
|
fields[1].hint = _("Catalog name")
|
|
fields[2].hint = _("Catalog URL")
|
|
if item then
|
|
title = _("Edit OPDS catalog")
|
|
fields[1].text = item.text
|
|
fields[2].text = item.url
|
|
fields[3].text = item.username
|
|
fields[4].text = item.password
|
|
else
|
|
title = _("Add OPDS catalog")
|
|
end
|
|
end
|
|
fields[3].hint = _("Username (optional)")
|
|
fields[4].hint = _("Password (optional)")
|
|
fields[4].text_type = "password"
|
|
|
|
local dialog
|
|
dialog = MultiInputDialog:new{
|
|
title = title,
|
|
fields = fields,
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
end
|
|
},
|
|
{
|
|
text = _("Save"),
|
|
callback = function()
|
|
local dialog_fields = dialog:getFields()
|
|
if is_calibre then
|
|
self:editCalibreFromInput(dialog_fields)
|
|
else
|
|
self:editCatalogFromInput(dialog_fields, item)
|
|
end
|
|
UIManager:close(dialog)
|
|
end
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
dialog:onShowKeyboard()
|
|
end
|
|
|
|
-- Shows dialog to add a subcatalog to the root list
|
|
function OPDSBrowser:addSubCatalog(item_url)
|
|
local dialog
|
|
dialog = InputDialog:new{
|
|
title = _("Add OPDS catalog"),
|
|
input = self.root_catalog_title .. " - " .. self.catalog_title,
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Save"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
local name = dialog:getInputText()
|
|
if name ~= "" then
|
|
UIManager:close(dialog)
|
|
local fields = {name, item_url, self.root_catalog_username, self.root_catalog_password}
|
|
self:editCatalogFromInput(fields, false, true) -- no init, stay in the subcatalog
|
|
end
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
dialog:onShowKeyboard()
|
|
end
|
|
|
|
-- Saves catalog properties from input dialog
|
|
function OPDSBrowser:editCatalogFromInput(fields, item, no_init)
|
|
local new_server
|
|
if item then -- edit old
|
|
for _, server in ipairs(self.opds_servers) do
|
|
if server.title == item.text and server.url == item.url then
|
|
new_server = server
|
|
break
|
|
end
|
|
end
|
|
else -- add new
|
|
new_server = {}
|
|
end
|
|
new_server.title = fields[1]
|
|
new_server.url = fields[2]:match("^%a+://") and fields[2] or "http://" .. fields[2]
|
|
new_server.username = fields[3] ~= "" and fields[3] or nil
|
|
new_server.password = fields[4]
|
|
if not item then
|
|
table.insert(self.opds_servers, new_server)
|
|
end
|
|
if not no_init then
|
|
self:init()
|
|
end
|
|
end
|
|
|
|
-- Saves calibre properties from input dialog
|
|
function OPDSBrowser:editCalibreFromInput(fields)
|
|
self.calibre_opds.host = fields[1]
|
|
if tonumber(fields[2]) then
|
|
self.calibre_opds.port = fields[2]
|
|
end
|
|
self.calibre_opds.username = fields[3] ~= "" and fields[3] or nil
|
|
self.calibre_opds.password = fields[4]
|
|
self:init()
|
|
end
|
|
|
|
-- Deletes catalog from the root list
|
|
function OPDSBrowser:deleteCatalog(item)
|
|
for i, server in ipairs(self.opds_servers) do
|
|
if server.title == item.text and server.url == item.url then
|
|
table.remove(self.opds_servers, i)
|
|
break
|
|
end
|
|
end
|
|
self:init()
|
|
end
|
|
|
|
-- Fetches feed from server
|
|
function OPDSBrowser:fetchFeed(item_url, headers_only)
|
|
local sink = {}
|
|
socketutil:set_timeout(socketutil.LARGE_BLOCK_TIMEOUT, socketutil.LARGE_TOTAL_TIMEOUT)
|
|
local request = {
|
|
url = item_url,
|
|
method = headers_only and "HEAD" 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 = self.root_catalog_username,
|
|
password = self.root_catalog_password,
|
|
}
|
|
logger.dbg("Request:", request)
|
|
local code, headers, status = socket.skip(1, http.request(request))
|
|
socketutil:reset_timeout()
|
|
|
|
if headers_only then
|
|
return headers and headers["last-modified"]
|
|
end
|
|
if code == 200 then
|
|
local xml = table.concat(sink)
|
|
return xml ~= "" and xml
|
|
end
|
|
|
|
local text, icon
|
|
if headers and code == 301 then
|
|
text = T(_("The catalog has been permanently moved. Please update catalog URL to '%1'."), BD.url(headers.location))
|
|
elseif headers and code == 302
|
|
and item_url:match("^https")
|
|
and headers.location:match("^http[^s]") then
|
|
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"
|
|
else
|
|
local error_message = {
|
|
["401"] = _("Authentication required for catalog. Please add a username and password."),
|
|
["403"] = _("Failed to authenticate. Please check your username and password."),
|
|
["404"] = _("Catalog not found."),
|
|
["406"] = _("Cannot get catalog. Server refuses to serve uncompressed content."),
|
|
}
|
|
text = code and error_message[tostring(code)] or T(_("Cannot get catalog. Server response status: %1."), status or code)
|
|
end
|
|
UIManager:show(InfoMessage:new{
|
|
text = text,
|
|
icon = icon,
|
|
})
|
|
end
|
|
|
|
-- Parses feed to catalog
|
|
function OPDSBrowser:parseFeed(item_url)
|
|
local feed_last_modified = self:fetchFeed(item_url, true) -- headers only
|
|
local feed
|
|
if feed_last_modified then
|
|
local hash = "opds|catalog|" .. item_url .. "|" .. feed_last_modified
|
|
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)
|
|
if feed then
|
|
logger.dbg("Caching", hash)
|
|
CatalogCache:insert(hash, feed)
|
|
end
|
|
end
|
|
else
|
|
feed = self:fetchFeed(item_url)
|
|
end
|
|
if feed then
|
|
return OPDSParser:parse(feed)
|
|
end
|
|
end
|
|
|
|
-- Generates link to search in catalog
|
|
function OPDSBrowser:getSearchTemplate(osd_url)
|
|
-- parse search descriptor
|
|
local search_descriptor = self:parseFeed(osd_url)
|
|
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
|
|
|
|
-- Generates menu items from the fetched list of catalog entries
|
|
function OPDSBrowser:genItemTableFromURL(item_url)
|
|
local ok, catalog = pcall(self.parseFeed, self, item_url)
|
|
if not ok then
|
|
logger.info("Cannot get catalog info from", item_url, catalog)
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Cannot get catalog info from %1"), (item_url and BD.url(item_url) or "nil")),
|
|
})
|
|
catalog = nil
|
|
end
|
|
return self:genItemTableFromCatalog(catalog, item_url)
|
|
end
|
|
|
|
function OPDSBrowser:genItemTableFromCatalog(catalog, item_url)
|
|
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
|
|
table.insert(item_table, { -- the first item in each subcatalog
|
|
text = "\u{f002} " .. _("Search"), -- append SEARCH icon
|
|
url = build_href(self:getSearchTemplate(build_href(link.href))),
|
|
searchable = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
item_table.hrefs = hrefs
|
|
|
|
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
|
|
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: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
|
|
for k, v in pairs(link) do
|
|
if k:sub(-6) == ":count" then
|
|
count = tonumber(v)
|
|
break
|
|
end
|
|
end
|
|
if count then
|
|
table.insert(item.acquisitions, {
|
|
type = link.type,
|
|
href = link_href,
|
|
title = link.title,
|
|
count = count,
|
|
})
|
|
end
|
|
elseif link.rel == self.thumbnail_rel then
|
|
item.thumbnail = link_href
|
|
elseif link.rel == self.image_rel then
|
|
item.image = 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
|
|
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
|
|
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.content = entry.content or entry.summary
|
|
table.insert(item_table, item)
|
|
end
|
|
return item_table
|
|
end
|
|
|
|
-- Requests and shows updated list of catalog entries
|
|
function OPDSBrowser:updateCatalog(item_url, paths_updated)
|
|
local menu_table = self:genItemTableFromURL(item_url)
|
|
if #menu_table > 0 then
|
|
if not paths_updated then
|
|
table.insert(self.paths, {
|
|
url = item_url,
|
|
title = self.catalog_title,
|
|
})
|
|
end
|
|
self:switchItemTable(self.catalog_title, menu_table)
|
|
self.onLeftButtonTap = function()
|
|
self:addSubCatalog(item_url)
|
|
end
|
|
if self.page_num <= 1 then
|
|
self:onNext()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Requests and adds more catalog entries to fill out the page
|
|
function OPDSBrowser:appendCatalog(item_url)
|
|
local menu_table = self:genItemTableFromURL(item_url)
|
|
if #menu_table > 0 then
|
|
for _, item in ipairs(menu_table) do
|
|
table.insert(self.item_table, item)
|
|
end
|
|
self.item_table.hrefs = menu_table.hrefs
|
|
self:switchItemTable(self.catalog_title, self.item_table, -1)
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Shows dialog to search in catalog
|
|
function OPDSBrowser:searchCatalog(item_url)
|
|
local dialog
|
|
dialog = InputDialog:new{
|
|
title = _("Search OPDS catalog"),
|
|
-- @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"),
|
|
description = _("%s in url will be replaced by your input"),
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Search"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
self.catalog_title = _("Search results")
|
|
local search_str = dialog:getInputText():gsub(" ", "+")
|
|
self:updateCatalog(item_url:gsub("%%s", search_str))
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
dialog:onShowKeyboard()
|
|
end
|
|
|
|
-- Shows dialog to download / stream a book
|
|
function OPDSBrowser:showDownloads(item)
|
|
local acquisitions = item.acquisitions
|
|
local filename = item.title
|
|
if item.author then
|
|
filename = item.author .. " - " .. filename
|
|
end
|
|
local filename_orig = filename
|
|
|
|
local function createTitle(path, file) -- title for ButtonDialogTitle
|
|
return T(_("Download folder:\n%1\n\nDownload filename:\n%2\n\nDownload file type:"),
|
|
BD.dirpath(path), file)
|
|
end
|
|
|
|
local buttons = {} -- buttons for ButtonDialogTitle
|
|
local stream_buttons -- page stream buttons
|
|
local download_buttons = {} -- file type download buttons
|
|
|
|
for i, acquisition in ipairs(acquisitions) do -- filter out unsupported file types
|
|
if acquisition.count then
|
|
stream_buttons = {
|
|
{
|
|
-- @translators "Stream" here refers to being able to read documents from an OPDS server without downloading them completely, on a page by page basis.
|
|
text = _("Page stream") .. "\u{2B0C}", -- append LEFT RIGHT BLACK ARROW
|
|
callback = function()
|
|
OPDSPSE:streamPages(acquisition.href, acquisition.count, false, self.root_catalog_username, self.root_catalog_password)
|
|
UIManager:close(self.download_dialog)
|
|
end,
|
|
},
|
|
{
|
|
-- @translators "Stream" here refers to being able to read documents from an OPDS server without downloading them completely, on a page by page basis.
|
|
text = _("Stream from page") .. "\u{2B0C}", -- append LEFT RIGHT BLACK ARROW
|
|
callback = function()
|
|
OPDSPSE:streamPages(acquisition.href, acquisition.count, true, self.root_catalog_username, self.root_catalog_password)
|
|
UIManager:close(self.download_dialog)
|
|
end,
|
|
},
|
|
}
|
|
else
|
|
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 -- supported file type
|
|
local text = url.unescape(acquisition.title or string.upper(filetype))
|
|
table.insert(download_buttons, {
|
|
text = text .. "\u{2B07}", -- append DOWNWARDS BLACK ARROW
|
|
callback = function()
|
|
self:downloadFile(filename .. "." .. string.lower(filetype), acquisition.href)
|
|
UIManager:close(self.download_dialog)
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
local buttons_nb = #download_buttons
|
|
if buttons_nb > 0 then
|
|
if buttons_nb == 1 then -- one wide button
|
|
table.insert(buttons, download_buttons)
|
|
else
|
|
if buttons_nb % 2 == 1 then -- we need even number of buttons
|
|
table.insert(download_buttons, {text = ""})
|
|
end
|
|
for i = 1, buttons_nb, 2 do -- two buttons in a row
|
|
table.insert(buttons, {download_buttons[i], download_buttons[i+1]})
|
|
end
|
|
end
|
|
table.insert(buttons, {}) -- separator
|
|
end
|
|
if stream_buttons then
|
|
table.insert(buttons, stream_buttons)
|
|
table.insert(buttons, {}) -- separator
|
|
end
|
|
table.insert(buttons, { -- action buttons
|
|
{
|
|
text = _("Choose folder"),
|
|
callback = function()
|
|
require("ui/downloadmgr"):new{
|
|
onConfirm = function(path)
|
|
logger.dbg("Download folder set to", path)
|
|
G_reader_settings:saveSetting("download_dir", path)
|
|
self.download_dialog:setTitle(createTitle(path, filename))
|
|
end,
|
|
}:chooseDir(self.getCurrentDownloadDir())
|
|
end,
|
|
},
|
|
{
|
|
text = _("Change filename"),
|
|
callback = function()
|
|
local dialog
|
|
dialog = InputDialog:new{
|
|
title = _("Enter filename"),
|
|
input = filename,
|
|
input_hint = filename_orig,
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
id = "close",
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Set filename"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
filename = dialog:getInputValue()
|
|
if filename == "" then
|
|
filename = filename_orig
|
|
end
|
|
UIManager:close(dialog)
|
|
self.download_dialog:setTitle(createTitle(self.getCurrentDownloadDir(), filename))
|
|
end,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
dialog:onShowKeyboard()
|
|
end,
|
|
},
|
|
})
|
|
table.insert(buttons, {
|
|
{
|
|
text = _("Cancel"),
|
|
callback = function()
|
|
UIManager:close(self.download_dialog)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Book information"),
|
|
enabled = type(item.content) == "string",
|
|
callback = function()
|
|
local TextViewer = require("ui/widget/textviewer")
|
|
UIManager:show(TextViewer:new{
|
|
title = item.text,
|
|
title_multilines = true,
|
|
text = util.htmlToPlainTextIfHtml(item.content),
|
|
text_face = Font:getFace("x_smallinfofont", G_reader_settings:readSetting("items_font_size")),
|
|
})
|
|
end,
|
|
},
|
|
})
|
|
|
|
self.download_dialog = ButtonDialogTitle:new{
|
|
title = createTitle(self.getCurrentDownloadDir(), filename),
|
|
buttons = buttons,
|
|
}
|
|
UIManager:show(self.download_dialog)
|
|
end
|
|
|
|
-- Returns user selected or last opened folder
|
|
function OPDSBrowser.getCurrentDownloadDir()
|
|
return G_reader_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir")
|
|
end
|
|
|
|
-- Downloads a book (with "File already exists" dialog)
|
|
function OPDSBrowser:downloadFile(filename, remote_url)
|
|
local download_dir = self.getCurrentDownloadDir()
|
|
|
|
filename = util.getSafeFilename(filename, download_dir)
|
|
local local_path = (download_dir ~= "/" and download_dir or "") .. '/' .. 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, status
|
|
if parsed.scheme == "http" or parsed.scheme == "https" then
|
|
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
|
|
code, headers, status = socket.skip(1, http.request {
|
|
url = remote_url,
|
|
headers = {
|
|
["Accept-Encoding"] = "identity",
|
|
},
|
|
sink = ltn12.sink.file(io.open(local_path, "w")),
|
|
user = self.root_catalog_username,
|
|
password = self.root_catalog_password,
|
|
})
|
|
socketutil:reset_timeout()
|
|
else
|
|
UIManager:show(InfoMessage:new {
|
|
text = T(_("Invalid protocol:\n%1"), parsed.scheme),
|
|
})
|
|
end
|
|
|
|
if code == 200 then
|
|
logger.dbg("File downloaded to", local_path)
|
|
self.file_downloaded_callback(local_path)
|
|
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)
|
|
logger.dbg("OPDSBrowser:downloadFile: Request failed:", status or code)
|
|
logger.dbg("OPDSBrowser:downloadFile: Response headers:", headers)
|
|
UIManager:show(InfoMessage:new {
|
|
text = T(_("Could not save file to:\n%1\n%2"),
|
|
BD.filepath(local_path),
|
|
status or code or "network unreachable"),
|
|
})
|
|
end
|
|
end)
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Downloading may take several minutes…"),
|
|
timeout = 1,
|
|
})
|
|
end
|
|
|
|
if lfs.attributes(local_path) 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
|
|
|
|
-- Menu action on item tap (Download a book / Show subcatalog / Search in catalog)
|
|
function OPDSBrowser:onMenuSelect(item)
|
|
if item.acquisitions and item.acquisitions[1] then -- book
|
|
logger.dbg("Downloads available:", item)
|
|
self:showDownloads(item)
|
|
else -- catalog or Search item
|
|
if #self.paths == 0 then -- root list
|
|
self.root_catalog_title = item.text
|
|
self.root_catalog_username = item.username
|
|
self.root_catalog_password = item.password
|
|
end
|
|
local connect_callback
|
|
if item.searchable then
|
|
connect_callback = function()
|
|
self:searchCatalog(item.url)
|
|
end
|
|
else
|
|
self.catalog_title = item.text or self.catalog_title or self.root_catalog_title
|
|
connect_callback = function()
|
|
self:updateCatalog(item.url)
|
|
end
|
|
end
|
|
NetworkMgr:runWhenConnected(connect_callback)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Menu action on item long-press (dialog Edit / Delete catalog)
|
|
function OPDSBrowser:onMenuHold(item)
|
|
if #self.paths > 0 then return end -- not root list
|
|
local is_calibre = item.text == self.calibre_name
|
|
local dialog
|
|
dialog = ButtonDialogTitle:new{
|
|
title = item.text,
|
|
title_align = "center",
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Edit"),
|
|
callback = function()
|
|
UIManager:close(dialog)
|
|
self:addEditCatalog(item, is_calibre)
|
|
end,
|
|
},
|
|
{
|
|
text = _("Delete"),
|
|
enabled = not is_calibre,
|
|
callback = function()
|
|
UIManager:show(ConfirmBox:new{
|
|
text = _("Delete OPDS catalog?"),
|
|
ok_text = _("Delete"),
|
|
ok_callback = function()
|
|
UIManager:close(dialog)
|
|
self:deleteCatalog(item)
|
|
end,
|
|
})
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(dialog)
|
|
return true
|
|
end
|
|
|
|
-- Menu action on return-arrow tap (go to one-level upper catalog)
|
|
function OPDSBrowser:onReturn()
|
|
if #self.paths > 0 then -- not root list
|
|
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, true)
|
|
else
|
|
-- return to root path, we simply reinit opdsbrowser
|
|
self:init()
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Menu action on return-arrow long-press (go to the catalog home page)
|
|
function OPDSBrowser:onHoldReturn()
|
|
if #self.paths > 1 then -- not catalog home page
|
|
local path = self.paths[1]
|
|
for i = #self.paths, 2, -1 do
|
|
table.remove(self.paths)
|
|
end
|
|
self.catalog_title = path.title
|
|
self:updateCatalog(path.url, true)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Menu action on next-page chevron tap (request and show more catalog entries)
|
|
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) then
|
|
break -- reach end of paging
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
return OPDSBrowser
|