add OPDS support

This PR implements a simple OPDS browser which can be launched
from filemanager menu.
pull/876/head
chrox 10 years ago
parent 69919435ac
commit 21dcf787da

@ -31,6 +31,14 @@ function FileManagerMenu:init()
tools = {
icon = "resources/icons/appbar.tools.png",
},
opdscatalog = {
icon = "resources/icons/appbar.magnify.browse.png",
callback = function()
self:onCloseFileManagerMenu()
local OPDSCatalog = require("apps/opdscatalog/opdscatalog")
OPDSCatalog:showCatalog()
end,
},
home = {
icon = "resources/icons/appbar.home.png",
callback = function()
@ -182,6 +190,7 @@ function FileManagerMenu:onShowMenu()
self.tab_item_table.setting,
self.tab_item_table.info,
self.tab_item_table.tools,
self.tab_item_table.opdscatalog,
self.tab_item_table.home,
},
show_parent = menu_container,
@ -212,6 +221,11 @@ function FileManagerMenu:onShowMenu()
return true
end
function FileManagerMenu:onCloseFileManagerMenu()
UIManager:close(self.menu_container)
return true
end
function FileManagerMenu:onTapShowMenu()
self:onShowMenu()
return true

@ -0,0 +1,86 @@
local InputContainer = require("ui/widget/container/inputcontainer")
local FrameContainer = require("ui/widget/container/framecontainer")
local FileManagerMenu = require("apps/filemanager/filemanagermenu")
local DocumentRegistry = require("document/documentregistry")
local VerticalGroup = require("ui/widget/verticalgroup")
local ButtonDialog = require("ui/widget/buttondialog")
local VerticalSpan = require("ui/widget/verticalspan")
local OPDSBrowser = require("ui/widget/opdsbrowser")
local TextWidget = require("ui/widget/textwidget")
local lfs = require("libs/libkoreader-lfs")
local UIManager = require("ui/uimanager")
local Font = require("ui/font")
local Screen = require("ui/screen")
local Geom = require("ui/geometry")
local Event = require("ui/event")
local DEBUG = require("dbg")
local _ = require("gettext")
local util = require("ffi/util")
local OPDSCatalog = InputContainer:extend{
title = _("OPDS Catalog"),
opds_servers = {
{
title = "Project Gutenberg",
subtitle = "Free ebooks since 1971.",
url = "http://m.gutenberg.org/ebooks.opds/?format=opds",
},
{
title = "Feedbooks",
subtitle = "",
url = "http://www.feedbooks.com/publicdomain/catalog.atom",
},
{
title = "ManyBooks",
subtitle = "Online Catalog for Manybooks.net",
url = "http://manybooks.net/opds/index.php",
},
{
title = "Internet Archive",
subtitle = "Internet Archive Catalog",
baseurl = "http://bookserver.archive.org/catalog/",
},
},
onExit = function() end,
}
function OPDSCatalog:init()
local opds_browser = OPDSBrowser:new{
opds_servers = self.opds_servers,
title = self.title,
show_parent = self,
is_popout = false,
is_borderless = true,
has_close_button = true,
close_callback = function() return self:onClose() end,
}
self[1] = FrameContainer:new{
padding = 0,
bordersize = 0,
background = 0,
opds_browser,
}
end
function OPDSCatalog:showCatalog()
DEBUG("show OPDS catalog")
UIManager:show(OPDSCatalog:new{
dimen = Screen:getSize(),
onExit = function()
--UIManager:quit()
end
})
end
function OPDSCatalog:onClose()
DEBUG("close OPDS catalog")
UIManager:close(self)
if self.onExit then
self:onExit()
end
return true
end
return OPDSCatalog

@ -190,6 +190,11 @@ function ReaderMenu:onShowReaderMenu()
return true
end
function ReaderMenu:onCloseReaderMenu()
UIManager:close(self.menu_container)
return true
end
function ReaderMenu:onTapShowMenu()
self.ui:handleEvent(Event:new("ShowConfigMenu"))
self.ui:handleEvent(Event:new("ShowReaderMenu"))

@ -0,0 +1,436 @@
--[[
This code was derived from the pico_xml project
which can be found here:
http://kd7yhr.org/bushbo/pico_xml.md
The original C code, written by: Brian O. Bush
Pure Lua Version written by: William A Adams
Dramatic Speed Improvements by: Robert G Jakabosky
References
http://www.faqs.org/rfcs/rfc3076.html
http://www.w3.org/TR/REC-xml/
]]
local ffi = require "ffi"
local bit = require "bit"
local band = bit.band
--[[
Types of characters; 0 is not valid, 1 is letters, 2 are digits
(including '.') and 3 whitespace.
--]]
local char_type = ffi.new("const int[256]", {
0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
});
-- Internal states that the parser can be in at any given time.
local ST_START = 0; -- starting base state; default state
local ST_TEXT =1; -- text state
local ST_START_TAG = 2; -- start tag state
local ST_START_TAGNAME =3; -- start tagname state
local ST_START_TAGNAME_END =4; -- start tagname ending state
local ST_END_TAG =5; -- end tag state
local ST_END_TAGNAME=6; -- end tag tagname state
local ST_END_TAGNAME_END=7; -- end tag tagname ending
local ST_EMPTY_TAG=8; -- empty tag state
local ST_SPACE=9; -- linear whitespace state
local ST_ATTR_NAME=10; -- attribute name state
local ST_ATTR_NAME_END=11; -- attribute name ending state
local ST_ATTR_VAL=12; -- attribute value starting state
local ST_ATTR_VAL2=13; -- attribute value state
local ST_ERROR=14; -- error state
-- character classes that we will match against; This could be expanded if
-- need be, however, we are aiming for simple.
local CCLASS_NONE = 0; -- matches nothing; a base state
local CCLASS_LEFT_ANGLE=1; -- matches start tag '<'
local CCLASS_SLASH=2; -- matches forward slash
local CCLASS_RIGHT_ANGLE=3; -- matches end tag '>'
local CCLASS_EQUALS=4; -- matches equals sign
local CCLASS_QUOTE=5; -- matches double-quotes
local CCLASS_LETTERS=6; -- matches a-zA-Z letters and digits 0-9
local CCLASS_SPACE=7; -- matches whitespace
local CCLASS_ANY=8; -- matches any ASCII character; will match all above classes
-- Types of events: start element, end element, text, attr name, attr
-- val and start/end document. Other events can be ignored!
local EVENT_START = 0; -- Start tag
local EVENT_END = 1; -- End tag
local EVENT_TEXT = 2; -- Text
local EVENT_ATTR_NAME = 3; -- Attribute name
local EVENT_ATTR_VAL = 4; -- Attribute value
local EVENT_END_DOC = 5; -- End of document
local EVENT_MARK = 6; -- Internal only; notes position in buffer
local EVENT_NONE = 7; -- Internal only; should never see this event
local entity_refs = {
["&lt;"] = '<',
["&gt;"] = '>',
["&amp;"] = '&',
["&apos;"] = '\'',
["&quot;"] = '"',
}
-- Map constant values to constant names.
local STATE_NAMES = {
[ST_START] = "ST_START",
[ST_TEXT] = "ST_TEXT",
[ST_START_TAG] = "ST_START_TAG",
[ST_START_TAGNAME] = "ST_START_TAGNAME",
[ST_START_TAGNAME_END] = "ST_START_TAGNAME_END",
[ST_END_TAG] = "ST_END_TAG",
[ST_END_TAGNAME] = "ST_END_TAGNAME",
[ST_END_TAGNAME_END] = "ST_END_TAGNAME_END",
[ST_EMPTY_TAG] = "ST_EMPTY_TAG",
[ST_SPACE] = "ST_SPACE",
[ST_ATTR_NAME] = "ST_ATTR_NAME",
[ST_ATTR_NAME_END] = "ST_ATTR_NAME_END",
[ST_ATTR_VAL] = "ST_ATTR_VAL",
[ST_ATTR_VAL2] = "ST_ATTR_VAL2",
[ST_ERROR] = "ST_ERROR",
}
--[[
State transition table element; contains:
(1) current state,
(2) clazz that must match,
(3) next state if we match, and
(4) event that is emitted upon match.
--]]
-- Note: States must be grouped in match order AND grouped together!
local LEXER_STATES = {
-- [0-2] starting state, which also serves as the default state in case of error
{ state = ST_START, cclass = CCLASS_SPACE, next_state = ST_SPACE, event = EVENT_NONE },
{ state = ST_START, cclass = CCLASS_LEFT_ANGLE, next_state = ST_START_TAG, event = EVENT_NONE },
{ state = ST_START, cclass = CCLASS_ANY, next_state = ST_TEXT, event = EVENT_MARK },
-- [3-5] space state handles linear white space
{ state = ST_SPACE, cclass = CCLASS_SPACE, next_state = ST_SPACE, event = EVENT_NONE },
{ state = ST_SPACE, cclass = CCLASS_LEFT_ANGLE, next_state = ST_START_TAG, event = EVENT_TEXT },
{ state = ST_SPACE, cclass = CCLASS_ANY, next_state = ST_TEXT, event = EVENT_MARK },
-- [6-8] handle start tag
{ state = ST_START_TAG, cclass = CCLASS_LETTERS, next_state = ST_START_TAGNAME, event = EVENT_MARK },
{ state = ST_START_TAG, cclass = CCLASS_SLASH, next_state = ST_END_TAG, event = EVENT_MARK },
{ state = ST_START_TAG, cclass = CCLASS_SPACE, next_state = ST_START_TAG, event = EVENT_NONE }, -- < tag >
-- [9-12] handle start tag name
{ state = ST_START_TAGNAME, cclass = CCLASS_LETTERS, next_state = ST_START_TAGNAME, event = EVENT_NONE },
{ state = ST_START_TAGNAME, cclass = CCLASS_SPACE, next_state = ST_START_TAGNAME_END, event = EVENT_START },
{ state = ST_START_TAGNAME, cclass = CCLASS_SLASH, next_state = ST_EMPTY_TAG, event = EVENT_END },
{ state = ST_START_TAGNAME, cclass = CCLASS_RIGHT_ANGLE, next_state = ST_START, event = EVENT_START },
-- [13-16] handle start tag name end
{ state = ST_START_TAGNAME_END, cclass = CCLASS_LETTERS, next_state = ST_ATTR_NAME, event = EVENT_MARK },
{ state = ST_START_TAGNAME_END, cclass = CCLASS_SPACE, next_state = ST_START_TAGNAME_END, event = EVENT_NONE },
{ state = ST_START_TAGNAME_END, cclass = CCLASS_RIGHT_ANGLE, next_state = ST_START, event = EVENT_START },
{ state = ST_START_TAGNAME_END, cclass = CCLASS_SLASH, next_state = ST_EMPTY_TAG, event = EVENT_MARK }, -- Empty tag <br />
-- [17] handle empty tags, e.g., <br />
{ state = ST_EMPTY_TAG, cclass = CCLASS_RIGHT_ANGLE, next_state = ST_START, event = EVENT_END }, -- Empty tag <br />
-- [18] handle end tag, e.g., <tag />
{ state = ST_END_TAG, cclass = CCLASS_LETTERS, next_state = ST_END_TAGNAME, event = EVENT_NONE },
-- [19-21] handle end tag name
{ state = ST_END_TAGNAME, cclass = CCLASS_LETTERS, next_state = ST_END_TAGNAME, event = EVENT_NONE },
{ state = ST_END_TAGNAME, cclass = CCLASS_RIGHT_ANGLE, next_state = ST_START, event = EVENT_END },
{ state = ST_END_TAGNAME, cclass = CCLASS_SPACE, next_state = ST_END_TAGNAME_END, event = EVENT_END }, -- space after end tag name </br >
-- [22-23] handle ending of end tag name
{ state = ST_END_TAGNAME_END, cclass = CCLASS_SPACE, next_state = ST_END_TAGNAME_END, event = EVENT_NONE },
{ state = ST_END_TAGNAME_END, cclass = CCLASS_RIGHT_ANGLE,next_state = ST_START, event = EVENT_NONE },
-- [24-26] handle text
{ state = ST_TEXT, cclass = CCLASS_SPACE, next_state = ST_SPACE, event = EVENT_NONE },
{ state = ST_TEXT, cclass = CCLASS_LEFT_ANGLE, next_state = ST_START_TAG, event = EVENT_TEXT },
{ state = ST_TEXT, cclass = CCLASS_ANY, next_state = ST_TEXT, event = EVENT_NONE },
-- [27-29] handle attribute names
{ state = ST_ATTR_NAME, cclass = CCLASS_LETTERS, next_state = ST_ATTR_NAME, event = EVENT_MARK },
{ state = ST_ATTR_NAME, cclass = CCLASS_SPACE, next_state = ST_ATTR_NAME_END, event = EVENT_ATTR_NAME }, -- space before '=' sign
{ state = ST_ATTR_NAME, cclass = CCLASS_EQUALS, next_state = ST_ATTR_VAL, event = EVENT_ATTR_NAME }, -- <tag attr ="2">
-- [30-32] attribute name end
{ state = ST_ATTR_NAME_END, cclass = CCLASS_SPACE, next_state = ST_ATTR_NAME_END, event = EVENT_NONE },
{ state = ST_ATTR_NAME_END, cclass = CCLASS_LETTERS, next_state = ST_ATTR_NAME, event = EVENT_MARK },
{ state = ST_ATTR_NAME_END, cclass = CCLASS_EQUALS, next_state = ST_ATTR_VAL, event = EVENT_NONE },
-- [33-34] handle attribute values, initial quote and spaces
{ state = ST_ATTR_VAL, cclass = CCLASS_QUOTE, next_state = ST_ATTR_VAL2, event = EVENT_NONE },
{ state = ST_ATTR_VAL, cclass = CCLASS_SPACE, next_state = ST_ATTR_VAL, event = EVENT_NONE }, -- initial spaces before quoted attribute value
-- [35-37] handle actual attribute values
{ state = ST_ATTR_VAL2, cclass = CCLASS_QUOTE, next_state = ST_START_TAGNAME_END, event = EVENT_ATTR_VAL },
{ state = ST_ATTR_VAL2, cclass = CCLASS_LETTERS, next_state = ST_ATTR_VAL2, event = EVENT_MARK },
{ state = ST_ATTR_VAL2, cclass = CCLASS_SLASH, next_state = ST_ATTR_VAL2, event = EVENT_NONE },
-- [38] End of table marker
{ state = ST_ERROR, cclass = CCLASS_NONE, next_state = ST_ERROR, event = EVENT_NONE }
};
ffi.cdef[[
struct parse_state {
const char* buf; /* reference to buffer */
int bufsz; /* size of buf */
int mark;
int i;
int ix; /* index into buffer */
};
]]
local cclass_match = {
[CCLASS_LETTERS] = "(ctype == 1 or ctype == 2)",
[CCLASS_LEFT_ANGLE] = "(c == T_LT)",
[CCLASS_SLASH] = "(c == T_SLASH)",
[CCLASS_RIGHT_ANGLE] = "(c == T_GT)",
[CCLASS_EQUALS] = "(c == T_EQ)",
[CCLASS_QUOTE] = "(c == T_QUOTE)",
[CCLASS_SPACE] = "(ctype == 3)",
[CCLASS_ANY] = "true",
}
local STATES = {}
local STATE_FUNCS = {}
local function next_char(ps, state, verbose)
local i = ps.ix
if i >= ps.bufsz then
return EVENT_END_DOC, 0, i, state
end
ps.i = i
local c = band(ps.buf[i], 0xff);
ps.ix = i + 1
if verbose then
verbose(i, STATE_FUNCS[state], c)
end
-- run state function to find next state.
return state(ps, c, verbose)
end
local fsm_code = ''
local function code(...)
for i=1,select("#", ...) do
fsm_code = fsm_code .. tostring(select(i, ...))
end
end
code[[
local STATES, next_char, char_type = ...
local T_LT = string.byte('<')
local T_SLASH = string.byte('/')
local T_GT = string.byte('>')
local T_EQ = string.byte('=')
local T_QUOTE = string.byte('"')
]]
-- pre-define locals for state functions.
for i=0,#STATE_NAMES do
local name = STATE_NAMES[i]
code('local ', name, '_f\n')
end
-- group LEXER states.
for i=1,#LEXER_STATES do
local p_state = LEXER_STATES[i]
local state = STATES[p_state.state]
local cclasses
if not state then
cclasses = {}
state = { state = p_state.state, cclasses = cclasses }
STATES[p_state.state] = state
else
cclasses = state.cclasses
end
cclasses[#cclasses + 1] = p_state
end
local function gen_cclass_code(prefix, cclass)
local next_state = STATE_NAMES[cclass.next_state]
if cclass.event == EVENT_MARK then
code(prefix, "if(ps.mark == 0) then ps.mark = ps.i end -- mark the position\n")
elseif cclass.event ~= EVENT_NONE then
code(prefix, "if(ps.mark > 0) then\n")
code(prefix,' return ', cclass.event,', ',next_state,'_f\n')
code(prefix, "end\n")
end
code(prefix,'return next_char(ps, ', next_state,'_f, verbose)\n')
end
-- generate state functions.
for i=0,#STATE_NAMES do
local name = STATE_NAMES[i]
local state = STATES[i]
local cclasses = state.cclasses
code('function ', name, '_f(ps, c, verbose)\n')
code(' local ctype = char_type[c]\n')
local has_any
for i=1,#cclasses do
local cclass = cclasses[i]
local id = cclass.cclass
local condition = cclass_match[id]
if id == CCLASS_ANY then
has_any = cclass
elseif i == 1 then
code(' if ', condition, ' then\n')
gen_cclass_code(' ', cclass)
else
code(' elseif ', condition, ' then\n')
gen_cclass_code(' ', cclass)
end
end
code(' end\n')
-- catch-all for cclass_any or goto error state.
if has_any then
gen_cclass_code(' ', has_any)
else
-- exit from FSM on error.
code(' return nil, ',name,'_f, c\n')
end
code('end\n')
-- map state id to state function
code('STATES[', i, '] = ', name, '_f\n')
-- reverse map state function to state id
code('STATES[', name, '_f] = ', i, '\n')
end
-- Compile FSM code
local state_funcs = assert(loadstring(fsm_code, "luxl FSM code"))
state_funcs(STATE_FUNCS, next_char, char_type)
fsm_code = nil
local luxl = {
EVENT_START = EVENT_START; -- Start tag
EVENT_END = EVENT_END; -- End tag
EVENT_TEXT = EVENT_TEXT; -- Text
EVENT_ATTR_NAME = EVENT_ATTR_NAME; -- Attribute name
EVENT_ATTR_VAL = EVENT_ATTR_VAL; -- Attribute value
EVENT_END_DOC = EVENT_END_DOC; -- End of document
EVENT_MARK = EVENT_MARK; -- Internal only; notes position in buffer
EVENT_NONE = EVENT_NONE; -- Internal only; should never see this event
}
local luxl_mt = { __index = luxl }
function luxl.new(buffer, bufflen)
local newone = {
buf = ffi.cast("const uint8_t *", buffer); -- pointer to "uint8_t *" buffer (0 based)
bufsz = bufflen; -- size of input buffer
state = ST_START; -- current state
event = EVENT_NONE; -- current event
err = 0; -- number of errors thus far
markix = 0; -- offset of current item of interest
marksz = 0; -- size of current item of interest
MsgHandler = nil; -- Routine to handle messages
ErrHandler = nil; -- Routine to call when there's an error
EventHandler = nil;
ps = ffi.new('struct parse_state', {
buf = buffer,
bufsz = bufflen,
mark = 0,
i = 0,
ix = 0,
}),
}
setmetatable(newone, luxl_mt);
return newone;
end
function luxl:Reset(buffer, bufflen)
self.buf = buffer -- pointer to "uint8_t *" buffer (0 based)
self.bufsz = bufflen -- size of input buffer
self.state = ST_START -- current state
self.event = EVENT_NONE -- current event
self.err = 0 -- number of errors thus far
self.markix = 0 -- offset of current item of interest
self.marksz = 0 -- size of current item of interest
local ps = self.ps
ps.buf = buffer
ps.bufsz = bufflen
ps.mark = 0
ps.i = 0
ps.ix = 0
end
function luxl:SetMessageHandler(handler)
self.MsgHandler = handler;
end
--[[
GetNext is responsible for moving through the stream
of characters. At the moment, it's fairly naive in
terms of character encodings.
In a more robust implementation, luxl will read from a
stream, which knows about the specific encoding, and
will hand out code points based on that particular encoding.
So, only straight ASCII for the moment.
Returns event type, starting offset, size
--]]
function luxl:GetNext()
local event, state_f, c
local ps = self.ps
ps.mark = 0
state_f = STATE_FUNCS[self.state]
repeat
event, state_f, c = next_char(ps, state_f, self.MsgHandler)
-- update state id.
self.state = STATE_FUNCS[state_f]
if not event then
-- handle error
-- default to start state
self.err = self.err + 1;
if self.ErrHandler then
self.ErrHandler(ps.i, self.state, c);
end
end
until event
-- basically we are guaranteed never to have an event of
-- type EVENT_MARK or EVENT_NONE here.
self.event = event
local markix = ps.mark
local marksz = ps.i-ps.mark
self.markix = markix
self.marksz = marksz
if(self.EventHandler) then
self.EventHandler(event, markix, marksz)
end
return event, markix, marksz
end
function luxl:Lexemes()
return function()
local event, offset, size = self:GetNext();
if(event == EVENT_END_DOC) then
return nil;
else
return event, offset, size;
end
end
end
return luxl

@ -0,0 +1,93 @@
--[[
This code is derived from the LAPHLibs which can be found here:
https://github.com/Wiladams/LAPHLibs
--]]
local util = require("ffi/util")
local luxl = require("luxl")
local DEBUG = require("dbg")
local ffi = require("ffi")
local OPDSParser = {}
local unescape_map = {
["lt"] = "<",
["gt"] = ">",
["amp"] = "&",
["quot"] = '"',
["apos"] = "'"
}
local gsub, char = string.gsub, string.char
local function unescape(str)
return gsub(str, '(&(#?)([%d%a]+);)', function(orig, n, s)
return unescape_map[s] or n=="#" and util.unichar(tonumber(s)) or orig
end)
end
function OPDSParser:createFlatXTable(xlex, currentelement)
currentelement = currentelement or {}
local currentattributename = nil;
local attribute_count = 0;
-- start reading the thing
local txt = nil;
for event, offset, size in xlex:Lexemes() do
txt = ffi.string(xlex.buf + offset, size)
if event == luxl.EVENT_START and txt ~= "xml" then
-- does current element already have something
-- with this name?
-- if it does, if it's a table, add to it
-- if it doesn't, then add a table
local tab = self:createFlatXTable(xlex)
if txt == "entry" or txt == "link" then
if currentelement[txt] == nil then
currentelement[txt] = {}
end
table.insert(currentelement[txt], tab)
elseif type(currentelement) == "table" then
currentelement[txt] = tab
end
end
if event == luxl.EVENT_ATTR_NAME then
currentattributename = txt
end
if event == luxl.EVENT_ATTR_VAL then
currentelement[currentattributename] = txt
attribute_count = attribute_count + 1;
currentattributename = nil
end
if event == luxl.EVENT_TEXT then
--if attribute_count < 1 then
-- return txt
--end
currentelement = unescape(txt)
end
if event == luxl.EVENT_END then
return currentelement
end
end
return currentelement
end
function OPDSParser:parse(text)
-- luxl cannot properly handle xml comments and we need first remove them
text = text:gsub("<!--.--->", "")
-- luxl prefers <br />, other two forms are valid in HTML,
-- but will kick the ass of luxl
text = text:gsub("<br>", "<br />")
text = text:gsub("<br/>", "<br />")
local xlex = luxl.new(text, #text)
return self:createFlatXTable(xlex)
end
return OPDSParser

@ -0,0 +1,412 @@
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("ui/screen")
local Device = require("ui/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 = {},
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/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: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: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,
})
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("Network is unreachable")
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(catalog)
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 base_url then
return base_url .. "/" .. href
elseif item_url then
local parsed = url.parse(item_url)
-- 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 == 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)
return true
end
function OPDSBrowser:downloadFile(title, format, remote_url)
-- download to last opened dir
-- TODO: let the user select where to store the downloaded file?
local lastdir = G_reader_settings:readSetting("lastdir")
local local_path = lastdir .. "/" .. 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 is successfully saved to:\n") .. local_path,
timeout = 3,
})
else
DEBUG("response", {r, c, h})
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
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: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

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="48"
height="48"
viewBox="0 0 48 48"
enable-background="new 0 0 76.00 76.00"
xml:space="preserve"
id="svg2"
inkscape:version="0.48.4 r9939"
sodipodi:docname="appbar.arrow.enter.svg"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="1015"
id="namedview6"
showgrid="true"
inkscape:zoom="3.1052632"
inkscape:cx="69.958993"
inkscape:cy="-1.2649445"
inkscape:window-x="1280"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"><inkscape:grid
type="xygrid"
id="grid2989"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" /></sodipodi:namedview><path
d="M 12.36237,39 19.052264,39 44,39 l 0,-6.79015 -24.947736,8e-5 4.19e-4,-10.452024 8.361951,0 L 15.707317,9 4,21.757906 l 8.36237,0 0,13.847059"
id="path4"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;stroke-width:0.2;stroke-linejoin:round"
sodipodi:nodetypes="ccccccccccc" /></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="64"
height="64"
viewBox="0 0 64 64"
enable-background="new 0 0 76.00 76.00"
xml:space="preserve"
id="svg2"
inkscape:version="0.48.4 r9939"
sodipodi:docname="appbar.cabinet.files.svg"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="738"
id="namedview6"
showgrid="false"
inkscape:zoom="3.1052632"
inkscape:cx="38"
inkscape:cy="38"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><path
d="m 7.571429,32 0,-21.052632 6.428571,0 0,-2.6315785 6.428571,0 L 20.428571,7 43.25,7 56.428571,20.486842 56.428571,32 59,32 59,57 58.464243,57 5.535757,57 5,57 5,32 7.571429,32 z M 14,32 l 0,-17.105263 -2.571429,0 0,17.105263 L 14,32 z m 3.857143,-19.736842 0,19.736842 2.571428,0 0,-19.736842 -2.571428,0 z m 20.571428,30.263158 0,3.947368 -12.857142,0 0,-3.947368 12.857142,0 z M 52.571429,32 l 0,-6.578947 -14.142858,0 0,-14.473685 -14.142857,0 0,21.052632 28.285715,0 z m -10.285715,-20.065789 0,9.539473 9.321429,0 -9.321429,-9.539473 z m -32.142857,25.328947 0,14.473684 43.714286,0 0,-14.473684 -43.714286,0 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;stroke-width:0.2;stroke-linejoin:round" /></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -1,5 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" width="76.0106" height="76.0106" viewBox="0 0 76.01 76.01" enable-background="new 0 0 76.01 76.01" xml:space="preserve">
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 23.7533,55.4244L 23.7533,52.2573L 23.7533,38.322L 22.1698,39.5889L 19.0026,34.8382L 38.0053,20.5862L 45.9231,26.5245L 45.9231,22.1698L 49.0902,21.3781L 49.0902,28.8999L 57.0079,34.8382L 53.8408,39.5889L 52.2573,38.322L 52.2573,52.2573L 52.2573,55.4244L 23.7533,55.4244 Z M 38.0053,26.9204L 26.9204,35.7883L 26.9204,52.2573L 33.2546,52.2573L 33.2546,42.756L 42.756,42.756L 42.756,52.2573L 49.0902,52.2573L 49.0902,35.7883L 38.0053,26.9204 Z "/>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="64"
height="64"
viewBox="0 0 63.999497 63.999497"
enable-background="new 0 0 76.01 76.01"
xml:space="preserve"
id="svg2"
inkscape:version="0.48.4 r9939"
sodipodi:docname="appbar.home.svg"
inkscape:export-filename="/home/chrox/dev/koreader/resources/icons/src/appbar.home.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs8" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1680"
inkscape:window-height="1015"
id="namedview6"
showgrid="false"
inkscape:zoom="3.1048302"
inkscape:cx="38.649456"
inkscape:cy="38.005299"
inkscape:window-x="1280"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><path
d="m 11.749962,56.999552 0,-4.545405 0,-19.999872 -2.2499063,1.818248 -4.500095,-6.818181 26.9998593,-20.454397 11.249952,8.522617 0,-6.2498436 4.499952,-1.1362423 0,10.7952489 11.249813,8.522617 -4.499954,6.818181 -2.249905,-1.818248 0,19.999872 0,4.545405 -40.499716,0 z M 31.99982,16.090755 16.249914,28.817922 l 0,23.636225 8.999907,0 0,-13.636218 13.499999,0 0,13.636218 8.999904,0 0,-23.636225 L 31.99982,16.090755 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;stroke-width:0.2;stroke-linejoin:round" /></svg>

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" width="76" height="76" viewBox="0 0 76.00 76.00" enable-background="new 0 0 76.00 76.00" xml:space="preserve">
<path fill="#000000" fill-opacity="0.403922" stroke-width="0.2" stroke-linejoin="round" d="M 57,19L 57,26L 50,26L 50,19L 57,19 Z M 48,19L 48,26L 41,26L 41,19L 48,19 Z M 39,19L 39,26L 32,26L 32,19L 39,19 Z M 57,28L 57,35L 50,35L 50,28L 57,28 Z M 48,28L 48,35L 41,35L 41,28L 48,28 Z M 39,28L 39,35L 32,35L 32,28L 39,28 Z M 57,37L 57,44L 50,44L 50,37L 57,37 Z M 48,37L 48,44L 41,44L 41,37L 48,37 Z M 39,37L 39,44L 32,44L 32,37L 39,37 Z "/>
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 23.6506,56.2021C 22.5867,57.266 20.8618,57.266 19.7979,56.2021C 18.734,55.1382 18.734,53.4133 19.7979,52.3494L 27.6722,44.4751C 26.6112,42.7338 26,40.6883 26,38.5C 26,32.1487 31.1487,27 37.5,27C 43.8513,27 49,32.1487 49,38.5C 49,44.8513 43.8513,50 37.5,50C 35.3117,50 33.2662,49.3888 31.5249,48.3278L 23.6506,56.2021 Z M 37.5,31C 33.3579,31 30,34.3579 30,38.5C 30,42.6421 33.3579,46 37.5,46C 41.6421,46 45,42.6421 45,38.5C 45,34.3579 41.6421,31 37.5,31 Z "/>
</svg>

@ -0,0 +1,188 @@
local navigation_sample = [[
<?xml version="1.0" encoding="utf-8"?>
<!--
DON'T USE THIS PAGE FOR SCRAPING.
Seriously. You'll only get your IP blocked.
Download http://www.gutenberg.org/feeds/catalog.rdf.bz2 instead,
which contains *all* Project Gutenberg metadata in one RDF/XML file.
--><feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:relevance="http://a9.com/-/opensearch/extensions/relevance/1.0/" xml:base="http://m.gutenberg.org/ebooks.opds/?format=opds">
<id>http://m.gutenberg.org/ebooks.opds/</id>
<updated>2014-05-17T12:04:49Z</updated>
<title>Project Gutenberg</title>
<subtitle>Free ebooks since 1971.</subtitle>
<author>
<name>Marcello Perathoner</name>
<uri>http://www.gutenberg.org</uri>
<email>webmaster@gutenberg.org</email>
</author>
<icon>http://m.gutenberg.org/pics/favicon.png</icon>
<link rel="search" type="application/opensearchdescription+xml" title="Project Gutenberg Catalog Search" href="http://m.gutenberg.org/catalog/osd-books.xml"/>
<link rel="self" title="This Page" type="application/atom+xml;profile=opds-catalog" href="/ebooks.opds/"/>
<link rel="alternate" type="text/html" title="HTML Page" href="/ebooks/"/>
<link rel="start" title="Start Page" type="application/atom+xml;profile=opds-catalog" href="/ebooks.opds/"/>
<opensearch:itemsPerPage>25</opensearch:itemsPerPage>
<opensearch:startIndex>1</opensearch:startIndex>
<entry>
<updated>2014-05-17T12:04:49Z</updated>
<id>http://m.gutenberg.org/ebooks/search.opds/?sort_order=downloads</id>
<title>Popular</title>
<content type="text">Our most popular books.</content>
<link type="application/atom+xml;profile=opds-catalog" rel="subsection" href="/ebooks/search.opds/?sort_order=downloads"/>
<link type="image/png" rel="http://opds-spec.org/image/thumbnail" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAS4SURBVHjaYvz//z8DLQBAADEx0AgABABZAKb/AWYuLgAAAAAA+QEBAPoAACcE9fVaGwAAQhMBASUA/f0C8Pv74c7+/sTxDw+mHQYGGBXy8lcY/f01CQUFFPkAAPLj/f3Ux/LytOD4+KwPBwft+fr6AOn09AACCGzwP6ChP75/j9NiY2t0d3FRULa0ZOAREADa9pPh++3bDA83b2bYe+XKBzF2dm5rFxdWcRcXBmYREYZ/wGB8/fgxw7XduxmWnDhx4uC3b4Vff/488ebrVwaAAGJM19Ji+PbjR4aXouK0kNJSRhZ9fYhfYGHPwsLA8PYtA8PSpQwMKioMDG5uDAxMwBD89w8amED2t28Mr9atY+ibPv3FvGfPwl5//34YIICY9QQEdC14eBbHlZdzMSsqMjC8eMHA8P49BH/4wMDw7h0Dw+/fDAyGhgwMEhIMDG/eQMRg8p8+gc3nNjVlMGBk5Ll88aLOzW/fNgIEEAv3t28pHm5uQkwgr9+5w8AADBasAOYDmDwrKwMDGxvDv9evGb5dvMjw9ckTBhFg+Mbz85se/fIlCiCAWOSZmJxkhYUZGJ4/Z2D48YOI6AYazMXF8O/pU4Yf+/czfAbibyDDgVKgwDGUlWWQZGe3BAggFj4GBmlmYGCDwxFoI07XggwEuhAUtn/27mX4vmsXw4+XLxn+QtMsCyhlAeODFxjmvP//8wMEEMufv38//XvyRJCJnx+cCrAaCopAoKH/gUH1C2jor5s3Gf4gK4G6lpmZmeEPMIh+/f3LABBALPf//j376fZteQEhIZAMIrZh3gYq/P/9O8OvK1cY/pw7x/AXyAap+IuGQRYJ8vExXAfqefHr12uAAGIGev6PHiNjuAIwfEHpmfHPH0iQAPF/YDL6BUzHXw4cALvyL1AO3TAQ/R2IeYDhLgwM36kfPnzf+fHjXIAAYv7679/9T//+aQCTipYwMKx/A4PjFzAZfX/0iOHjpUsMn69eZfiD5so/UPwbaigvLy+DjLIywwZgjut++XLn+79/JwIEEPOv////3P316/CzX790FdjYVKSAYfr8+nWGV0CD/4IiFRp+/9BcCTIUFCNikpIMogoKDCuB6brx6dNjd3//bgIKXwIIIGZWiOIvl3792nLjyxdhMVZWY0N5eYa/QJd//vYNbjByEIDSzl9g7MsCcyKTuDjDxIcPf7c8f77xwd+/1UCp4yA9AAEEii4GZmDYAjX8eP3v345Lnz59Yf/718pcVZWNAxhxb4Au+YtkOCils3ByMqgCy5gX7OwMTdevv5/24cNcYF5sBkpdhcU7QAAxgFzMDs1NIkBX8ABpKQaGwFYBgWevra3/fzA0/H+Kmfk/0Bn/jwHxDWHh/38cHP4f19L6783M/BCoPA+IBdBTKUAAYRgsAmUDVRqnsbGduGlq+v+rjc3/03x8/+8qKPz/4eLyf6Ws7H9gyXESqCwYiNmw5SmAAMJqMKy0AGYZST9GxmXHNDX///X1/f/Byel/j4DAXxkGhjVAaVN8OR8ggHAaDAp7YVAeYWDgsAaG3zwxsfcFXFxvgUVAL1BYnlCRAhBAeA0WRfInLwODOzBjh4KUEVM1AQQQI60qU4AAAwBnu/BQIoGoSgAAAABJRU5ErkJggg=="/>
</entry>
<entry>
<updated>2014-05-17T12:04:49Z</updated>
<id>http://m.gutenberg.org/ebooks/search.opds/?sort_order=release_date</id>
<title>Latest</title>
<content type="text">Our latest releases.</content>
<link type="application/atom+xml;profile=opds-catalog" rel="subsection" href="/ebooks/search.opds/?sort_order=release_date"/>
<link type="image/png" rel="http://opds-spec.org/image/thumbnail" href="data:image/png;base64,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"/>
</entry>
<entry>
<updated>2014-05-17T12:04:49Z</updated>
<id>http://m.gutenberg.org/ebooks/search.opds/?sort_order=random</id>
<title>Random</title>
<content type="text">Random books.</content>
<link type="application/atom+xml;profile=opds-catalog" rel="subsection" href="/ebooks/search.opds/?sort_order=random"/>
<link type="image/png" rel="http://opds-spec.org/image/thumbnail" href="data:image/png;base64,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"/>
</entry>
</feed>
]]
local acquisition_sample = [[
<?xml version="1.0" encoding="utf-8"?>
<!--
DON'T USE THIS PAGE FOR SCRAPING.
Seriously. You'll only get your IP blocked.
Download http://www.gutenberg.org/feeds/catalog.rdf.bz2 instead,
which contains *all* Project Gutenberg metadata in one RDF/XML file.
--><feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:relevance="http://a9.com/-/opensearch/extensions/relevance/1.0/" xml:base="http://m.gutenberg.org:80/ebooks/42474.opds">
<id>http://m.gutenberg.org:80/ebooks/42474.opds</id>
<updated>2014-09-03T12:16:15Z</updated>
<title>1000 Mythological Characters Briefly Described by Edward Sylvester Ellis</title>
<subtitle>Free ebooks since 1971.</subtitle>
<author>
<name>Marcello Perathoner</name>
<uri>http://www.gutenberg.org</uri>
<email>webmaster@gutenberg.org</email>
</author>
<icon>http://m.gutenberg.org:80/pics/favicon.png</icon>
<link rel="search" type="application/opensearchdescription+xml" title="Project Gutenberg Catalog Search" href="http://m.gutenberg.org:80/catalog/osd-books.xml"/>
<link rel="self" title="This Page" type="application/atom+xml;profile=opds-catalog" href="/ebooks/42474.opds"/>
<link rel="alternate" type="text/html" title="HTML Page" href="/ebooks/42474"/>
<link rel="start" title="Start Page" type="application/atom+xml;profile=opds-catalog" href="/ebooks.opds/"/>
<opensearch:itemsPerPage>25</opensearch:itemsPerPage>
<opensearch:startIndex>1</opensearch:startIndex>
<entry>
<updated>2014-09-03T12:16:15Z</updated>
<title>1000 Mythological Characters Briefly Described</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This edition had all images removed.</p>
<p>
Title:
1000 Mythological Characters Briefly Described<br />Adapted to Private Schools, High Schools and Academies
</p>
<p>Author: Ellis, Edward Sylvester, 1840-1916</p>
<p>Ebook No.: 42474</p>
<p>Published: Apr 7, 2013</p>
<p>Downloads: 1209</p>
<p>Language: English</p>
<p>Category: Text</p>
<p>Rights: Public domain in the USA.</p>
</div>
</content>
<id>urn:gutenberg:42474:2</id>
<published>2013-04-07T00:00:00+00:00</published>
<rights>Public domain in the USA.</rights>
<author>
<name>Ellis, Edward Sylvester</name>
</author>
<category scheme="http://purl.org/dc/terms/DCMIType" term="Text"/>
<dcterms:language>en</dcterms:language>
<relevance:score>1</relevance:score>
<link type="application/epub+zip" rel="http://opds-spec.org/acquisition" title="EPUB (no images)" length="144227" href="http://www.gutenberg.org/ebooks/42474.epub.noimages"/>
<link type="application/x-mobipocket-ebook" rel="http://opds-spec.org/acquisition" title="Kindle (no images)" length="550643" href="http://www.gutenberg.org/ebooks/42474.kindle.noimages"/>
<link type="image/jpeg" rel="http://opds-spec.org/image" href="http://www.gutenberg.org/cache/epub/42474/pg42474.cover.medium.jpg"/>
<link type="image/jpeg" rel="http://opds-spec.org/image/thumbnail" href="http://www.gutenberg.org/cache/epub/42474/pg42474.cover.small.jpg"/>
<link type="application/atom+xml;profile=opds-catalog" rel="related" href="/ebooks/42474/also/.opds" title="Readers also downloaded??"/>
<link type="application/atom+xml;profile=opds-catalog" rel="related" href="/ebooks/author/2569.opds" title="By Ellis, Edward Sylvester??"/>
</entry>
<entry>
<updated>2014-09-03T12:16:15Z</updated>
<title>1000 Mythological Characters Briefly Described</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This edition has images.</p>
<p>
Title:
1000 Mythological Characters Briefly Described<br />Adapted to Private Schools, High Schools and Academies
</p>
<p>Author: Ellis, Edward Sylvester, 1840-1916</p>
<p>Ebook No.: 42474</p>
<p>Published: Apr 7, 2013</p>
<p>Downloads: 1209</p>
<p>Language: English</p>
<p>Category: Text</p>
<p>Rights: Public domain in the USA.</p>
</div>
</content>
<id>urn:gutenberg:42474:3</id>
<published>2013-04-07T00:00:00+00:00</published>
<rights>Public domain in the USA.</rights>
<author>
<name>Ellis, Edward Sylvester</name>
</author>
<category scheme="http://purl.org/dc/terms/DCMIType" term="Text"/>
<dcterms:language>en</dcterms:language>
<relevance:score>1</relevance:score>
<link type="application/epub+zip" rel="http://opds-spec.org/acquisition" title="EPUB (with images)" length="647158" href="http://www.gutenberg.org/ebooks/42474.epub.images"/>
<link type="application/x-mobipocket-ebook" rel="http://opds-spec.org/acquisition" title="Kindle (with images)" length="1553578" href="http://www.gutenberg.org/ebooks/42474.kindle.images"/>
<link type="image/jpeg" rel="http://opds-spec.org/image" href="http://www.gutenberg.org/cache/epub/42474/pg42474.cover.medium.jpg"/>
<link type="image/jpeg" rel="http://opds-spec.org/image/thumbnail" href="http://www.gutenberg.org/cache/epub/42474/pg42474.cover.small.jpg"/>
<link type="application/atom+xml;profile=opds-catalog" rel="related" href="/ebooks/42474/also/.opds" title="Readers also downloaded??"/>
<link type="application/atom+xml;profile=opds-catalog" rel="related" href="/ebooks/author/2569.opds" title="By Ellis, Edward Sylvester??"/>
</entry>
</feed>
]]
package.path = "?.lua;common/?.lua;frontend/?.lua"
local OPDSParser = require("ui/opdsparser")
local DEBUG = require("dbg")
DEBUG:turnOn()
describe("OPDS parser module", function()
it("should parse OPDS navigation catalog", function()
local catalog = OPDSParser:parse(navigation_sample)
local feed = catalog.feed
assert.truthy(feed)
assert.are.same(feed.title, "Project Gutenberg")
local entries = feed.entry
assert.truthy(entries)
assert.are.same(#entries, 3)
local entry = entries[3]
assert.are.same(entry.title, "Random")
assert.are.same(entry.id, "http://m.gutenberg.org/ebooks/search.opds/?sort_order=random")
end)
it("should parse OPDS acquisition catalog", function()
local catalog = OPDSParser:parse(acquisition_sample)
local feed = catalog.feed
--DEBUG(feed)
assert.truthy(feed)
local entries = feed.entry
assert.truthy(entries)
assert.are.same(#entries, 2)
local entry = entries[2]
assert.are.same(entry.title, "1000 Mythological Characters Briefly Described")
end)
end)
Loading…
Cancel
Save