Files
koreader/frontend/ui/widget/opdsbrowser.lua
Hans-Werner Hilse 3066c86e38 Refactoring hardware abstraction
This is a major overhaul of the hardware abstraction layer.
A few notes:

General platform distinction happens in
  frontend/device.lua
which will delegate everything else to
  frontend/device/<platform_name>/device.lua
which should extend
  frontend/device/generic/device.lua

Screen handling is implemented in
  frontend/device/screen.lua
which includes the *functionality* to support device specifics.
Actually setting up the device specific functionality, however,
is done in the device specific setup code in the relevant
device.lua file.

The same goes for input handling.
2014-11-02 21:19:04 +01:00

621 lines
20 KiB
Lua

local MultiInputDialog = require("ui/widget/multiinputdialog")
local ButtonDialog = require("ui/widget/buttondialog")
local InfoMessage = require("ui/widget/infomessage")
local lfs = require("libs/libkoreader-lfs")
local OPDSParser = require("ui/opdsparser")
local NetworkMgr = require("ui/networkmgr")
local UIManager = require("ui/uimanager")
local CacheItem = require("cacheitem")
local Menu = require("ui/widget/menu")
local Screen = require("device").screen
local Device = require("device")
local url = require('socket.url')
local util = require("ffi/util")
local Cache = require("cache")
local DEBUG = require("dbg")
local _ = require("gettext")
local ffi = require("ffi")
local socket = require('socket')
local http = require('socket.http')
local https = require('ssl.https')
local ltn12 = require('ltn12')
local CatalogCacheItem = CacheItem:new{
size = 1024, -- fixed size for catalog item
}
-- cache catalog parsed from feed xml
local CatalogCache = Cache:new{
max_memsize = 20*1024, -- keep only 20 cache items
current_memsize = 0,
cache = {},
cache_order = {},
}
local OPDSBrowser = Menu:extend{
opds_servers = {},
calibre_name = _("Local calibre catalog"),
catalog_type = "application/atom%+xml",
search_type = "application/opensearchdescription%+xml",
acquisition_rel = "^http://opds%-spec%.org/acquisition",
thumbnail_rel = "http://opds-spec.org/image/thumbnail",
formats = {
["application/epub+zip"] = "EPUB",
["application/fb2+zip"] = "FB2",
["application/pdf"] = "PDF",
["text/plain"] = "TXT",
["application/x-mobipocket-ebook"] = "MOBI",
["application/x-mobi8-ebook"] = "AZW3",
},
width = Screen:getWidth(),
height = Screen:getHeight(),
no_title = false,
parent = nil,
}
function OPDSBrowser:init()
self.item_table = self:genItemTableFromRoot(self.opds_servers)
Menu.init(self) -- call parent's init()
end
function OPDSBrowser:addServerFromInput(fields)
DEBUG("input catalog", fields)
local servers = G_reader_settings:readSetting("opds_servers") or {}
table.insert(servers, {
title = fields[1],
url = fields[2],
})
G_reader_settings:saveSetting("opds_servers", servers)
self:init()
end
function OPDSBrowser:editCalibreFromInput(fields)
DEBUG("input calibre server", fields)
local calibre = G_reader_settings:readSetting("calibre_opds") or {}
if fields[1] then
calibre.host = fields[1]
end
if tonumber(fields[2]) then
calibre.port = fields[2]
end
G_reader_settings:saveSetting("calibre_opds", calibre)
self:init()
end
function OPDSBrowser:addNewCatalog()
self.add_server_dialog = MultiInputDialog:new{
title = _("Add OPDS catalog"),
fields = {
{
text = "",
hint = _("Catalog Name"),
},
{
text = "",
hint = _("Catalog URL"),
},
},
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
},
},
},
width = Screen:getWidth() * 0.95,
height = Screen:getHeight() * 0.2,
}
self.add_server_dialog:onShowKeyboard()
UIManager:show(self.add_server_dialog)
end
function OPDSBrowser:editCalibreServer()
local calibre = G_reader_settings:readSetting("calibre_opds") or {}
self.add_server_dialog = MultiInputDialog:new{
title = _("Edit local calibre host and port"),
fields = {
{
-- TODO: get IP address of current device
text = calibre.host or "192.168.1.1",
hint = _("Calibre host"),
},
{
text = calibre.port and tostring(calibre.port) or "8080",
hint = _("Calibre port"),
},
},
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
},
},
},
width = Screen:getWidth() * 0.95,
height = Screen:getHeight() * 0.2,
}
self.add_server_dialog:onShowKeyboard()
UIManager:show(self.add_server_dialog)
end
function OPDSBrowser:genItemTableFromRoot()
local item_table = {}
for i, server in ipairs(self.opds_servers) do
table.insert(item_table, {
text = server.title,
content = server.subtitle,
url = server.url,
baseurl = server.baseurl,
})
end
local added_servers = G_reader_settings:readSetting("opds_servers") or {}
for i, server in ipairs(added_servers) do
table.insert(item_table, {
text = server.title,
content = server.subtitle,
url = server.url,
baseurl = server.baseurl,
deletable = true,
editable = true,
})
end
local calibre_opds = G_reader_settings:readSetting("calibre_opds") or {}
local calibre_callback = nil
if not calibre_opds.host or not calibre_opds.port then
table.insert(item_table, {
text = self.calibre_name,
callback = function()
self:editCalibreServer()
end,
deletable = false,
})
else
table.insert(item_table, {
text = self.calibre_name,
url = string.format("http://%s:%d/opds",
calibre_opds.host, calibre_opds.port),
editable = true,
deletable = false,
})
end
table.insert(item_table, {
text = _("Add new OPDS catalog"),
callback = function()
self:addNewCatalog()
end,
})
return item_table
end
function OPDSBrowser:fetchFeed(feed_url)
local headers, request, sink = {}, {}, {}
local parsed = url.parse(feed_url)
request['url'] = feed_url
request['method'] = 'GET'
request['sink'] = ltn12.sink.table(sink)
DEBUG("request", request)
http.TIMEOUT, https.TIMEOUT = 10, 10
local httpRequest = parsed.scheme == 'http' and http.request or https.request
local code, headers, status = socket.skip(1, httpRequest(request))
-- raise error message when network is unavailable
if headers == nil then
error(code)
end
local xml = table.concat(sink)
if xml ~= "" then
--DEBUG("xml", xml)
return xml
end
end
function OPDSBrowser:parseFeed(feed_url)
local feed = nil
local hash = "opds|catalog|" .. feed_url
local cache = CatalogCache:check(hash)
if cache then
feed = cache.feed
else
DEBUG("cache", hash)
feed = self:fetchFeed(feed_url)
if feed then
local cache = CatalogCacheItem:new{
feed = feed
}
CatalogCache:insert(hash, cache)
end
end
if feed then
return OPDSParser:parse(feed)
end
end
function OPDSBrowser:getCatalog(feed_url)
local ok, catalog = pcall(self.parseFeed, self, feed_url)
-- prompt users to turn on Wifi if network is unreachable
if not ok and catalog and catalog:find("Network is unreachable") then
NetworkMgr:promptWifiOn()
return
elseif not ok and catalog then
DEBUG("cannot get catalog info from", feed_url, catalog)
UIManager:show(InfoMessage:new{
text = _("Cannot get catalog info from ") .. feed_url,
})
return
end
if ok and catalog then
DEBUG("catalog", catalog)
return catalog
end
end
function OPDSBrowser:genItemTableFromURL(item_url, base_url)
local item_table = {}
local catalog = self:getCatalog(item_url or base_url)
if catalog then
local feed = catalog.feed or catalog
local function build_href(href)
if href:match("^http") then
return href
elseif href:match("^//") then
local parsed = url.parse(item_url or base_url)
if parsed and parsed.scheme then
return parsed.scheme .. ":" .. href
else
return "http:" .. href
end
elseif base_url then
return base_url .. "/" .. href
elseif item_url then
local parsed = url.parse(item_url)
-- get rid of query field of base url
parsed.query = nil
-- update item url with href parts(mostly path and query)
for k, v in pairs(url.parse(href) or {}) do
if k == "path" then
v = "/" .. v
end
parsed[k] = v
end
return url.build(parsed)
end
end
local hrefs = {}
if feed.link then
for i, link in ipairs(feed.link) do
if link.type:find(self.catalog_type) or
link.type:find(self.search_type) then
if link.rel and link.href then
hrefs[link.rel] = build_href(link.href)
end
end
end
end
item_table.hrefs = hrefs
if feed.entry then
for i, entry in ipairs(feed.entry) do
local item = {}
item.baseurl = base_url
item.acquisitions = {}
if entry.link then
for i, link in ipairs(entry.link) do
if link.type:find(self.catalog_type) then
item.url = build_href(link.href)
end
if link.rel and link.rel:match(self.acquisition_rel) then
table.insert(item.acquisitions, {
type = link.type,
href = build_href(link.href),
})
end
if link.rel == self.thumbnail_rel then
item.thumbnail = build_href(link.href)
end
if link.rel == self.image_rel then
item.image = build_href(link.href)
end
end
end
local title = "Unknown"
local title_type = type(entry.title)
if type(entry.title) == "string" then
title = entry.title
elseif type(entry.title) == "table" then
if entry.title.type == "text/xhtml" then
title = entry.title.div or title
end
end
if title == "Unknown" then
DEBUG("Cannot handle title", entry.title)
end
item.text = title
item.title = title
item.id = entry.id
item.content = entry.content
item.updated = entry.updated
table.insert(item_table, item)
end
end
end
return item_table
end
function OPDSBrowser:updateCatalog(url, baseurl)
local menu_table = self:genItemTableFromURL(url, baseurl)
if #menu_table > 0 then
--DEBUG("menu table", menu_table)
self:swithItemTable(nil, menu_table)
return true
end
end
function OPDSBrowser:appendCatalog(url, baseurl)
local new_table = self:genItemTableFromURL(url, baseurl)
for i, item in ipairs(new_table) do
table.insert(self.item_table, item)
end
self.item_table.hrefs = new_table.hrefs
self:swithItemTable(nil, self.item_table, -1)
return true
end
function OPDSBrowser:downloadFile(title, format, remote_url)
-- download to user selected directory or last opened dir
local lastdir = G_reader_settings:readSetting("lastdir")
local download_dir = G_reader_settings:readSetting("download_dir") or lastdir
local local_path = download_dir .. "/" .. title .. "." .. string.lower(format)
DEBUG("downloading file", local_path, "from", remote_url)
local parsed = url.parse(remote_url)
http.TIMEOUT, https.TIMEOUT = 10, 10
local httpRequest = parsed.scheme == 'http' and http.request or https.request
local r, c, h = httpRequest{
url = remote_url,
sink = ltn12.sink.file(io.open(local_path, "w")),
}
if c == 200 then
DEBUG("file downloaded successfully to", local_path)
UIManager:show(InfoMessage:new{
text = _("File successfully saved to:\n") .. local_path,
timeout = 3,
})
else
DEBUG("response", {r, c, h})
UIManager:show(InfoMessage:new{
text = _("Could not save file to:\n") .. local_path,
timeout = 3,
})
end
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 format = self.formats[acquisition.type]
if format then
button.text = format
button.callback = function()
UIManager:scheduleIn(1, function()
self:downloadFile(item.title, format, acquisition.href)
end)
UIManager:close(self.download_dialog)
UIManager:show(InfoMessage:new{
text = _("Downloading may take several minutes..."),
timeout = 3,
})
end
table.insert(line, button)
end
end
end
table.insert(buttons, line)
end
-- set download directory button
table.insert(buttons, {
{
text = _("Set download directory"),
callback = function()
require("ui/downloadmgr"):new{
title = _("Choose download directory"),
onConfirm = function(path)
DEBUG("set download directory to", path)
G_reader_settings:saveSetting("download_dir", path)
end,
}:chooseDir()
end,
}
})
self.download_dialog = ButtonDialog:new{
buttons = buttons
}
UIManager:show(self.download_dialog)
end
function OPDSBrowser:onMenuSelect(item)
-- add catalog
if item.callback then
item.callback()
-- acquisition
elseif item.acquisitions and #item.acquisitions > 0 then
DEBUG("downloads available", item)
self:showDownloads(item)
-- navigation
else
table.insert(self.paths, {
url = item.url,
baseurl = item.baseurl,
})
if not self:updateCatalog(item.url, item.baseurl) then
table.remove(self.paths)
end
end
return true
end
function OPDSBrowser:editServerFromInput(item, fields)
DEBUG("input catalog", fields)
local servers = {}
for i, server in ipairs(G_reader_settings:readSetting("opds_servers") or {}) do
if server.title == item.text or server.url == item.url then
server.title = fields[1]
server.url = fields[2]
end
table.insert(servers, server)
end
G_reader_settings:saveSetting("opds_servers", servers)
self:init()
end
function OPDSBrowser:editOPDSServer(item)
DEBUG("edit", 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"),
},
},
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
},
},
},
width = Screen:getWidth() * 0.95,
height = Screen:getHeight() * 0.2,
}
self.edit_server_dialog:onShowKeyboard()
UIManager:show(self.edit_server_dialog)
end
function OPDSBrowser:deleteOPDSServer(item)
DEBUG("delete", item)
local servers = {}
for i, server in ipairs(G_reader_settings:readSetting("opds_servers") or {}) do
if server.title ~= item.text or server.url ~= item.url then
table.insert(servers, server)
end
end
G_reader_settings:saveSetting("opds_servers", servers)
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()
DEBUG("return to last page catalog")
if #self.paths > 0 then
table.remove(self.paths)
local path = self.paths[#self.paths]
if path then
-- return to last path
self:updateCatalog(path.url, path.baseurl)
else
-- return to root path, we simply reinit opdsbrowser
self:init()
end
end
return true
end
function OPDSBrowser:onNext()
DEBUG("fetch next page catalog")
local hrefs = self.item_table.hrefs
if hrefs and hrefs.next then
self:appendCatalog(hrefs.next)
end
return true
end
return OPDSBrowser