add OPDS support
This PR implements a simple OPDS browser which can be launched from filemanager menu.pull/876/head
@ -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
|
@ -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 = {
|
||||
["<"] = '<',
|
||||
[">"] = '>',
|
||||
["&"] = '&',
|
||||
["'"] = '\'',
|
||||
["""] = '"',
|
||||
}
|
||||
|
||||
-- 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
|
After Width: | Height: | Size: 429 B |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 737 B |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 984 B |
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 |
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 2.2 KiB |
@ -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)
|