CS: Implement /opds

Structure remains the same as the old server, except that there is
support for multiple libraries
This commit is contained in:
Kovid Goyal 2016-02-19 17:23:18 +05:30
parent 27527d4299
commit 2ef515b6c0
8 changed files with 648 additions and 20 deletions

View File

@ -1,4 +1,4 @@
Remove tweak to control interface to listen on
Remove all *content_server_* and server_listen_on tweaks
Rewrite server integration with nginx/apache section

View File

@ -18,7 +18,7 @@ from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.srv.errors import HTTPNotFound
from calibre.srv.routes import endpoint, json
from calibre.srv.content import get as get_content, icon as get_icon
from calibre.srv.utils import http_date, custom_fields_to_display, encode_name, decode_name
from calibre.srv.utils import http_date, custom_fields_to_display, encode_name, decode_name, get_db
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import isoformat, timestampfromdt
from calibre.utils.icu import numeric_sort_key as sort_key
@ -39,12 +39,6 @@ def get_pagination(query, num=100, offset=0):
raise HTTPNotFound("Invalid offset")
return num, offset
def get_db(ctx, library_id):
db = ctx.get_library(library_id)
if db is None:
raise HTTPNotFound('Library %r not found' % library_id)
return db
def category_icon(category, meta): # {{{
if category in category_icon_map:
icon = category_icon_map[category]

View File

@ -13,10 +13,11 @@ from calibre import prepare_string_for_xml, as_unicode
from calibre.constants import config_dir
from calibre.customize.ui import available_input_formats
from calibre.db.view import sanitize_sort_field_name
from calibre.srv.ajax import get_db, search_result
from calibre.srv.ajax import search_result
from calibre.srv.errors import HTTPNotFound, HTTPBadRequest
from calibre.srv.metadata import book_as_json, categories_as_json, icon_map
from calibre.srv.routes import endpoint, json
from calibre.srv.utils import get_library_data
from calibre.utils.config import prefs, tweaks
from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import ParseException
@ -61,14 +62,6 @@ def index(ctx, rd):
DEFAULT_LIBRARY=json_dumps(default_library)
))
def get_library_data(ctx, query):
library_id = query.get('library_id')
library_map, default_library = ctx.library_map
if library_id not in library_map:
library_id = default_library
db = get_db(ctx, library_id)
return db, library_id, library_map, default_library
def get_basic_query_data(ctx, query):
db, library_id, library_map, default_library = get_library_data(ctx, query)
skeys = db.field_metadata.sortable_field_keys()

View File

@ -173,7 +173,7 @@ class Handler(object):
prefer_basic_auth = {'auto':has_ssl, 'basic':True}.get(opts.auth_mode, 'digest')
self.auth_controller = AuthController(user_credentials=ctx.user_manager, prefer_basic_auth=prefer_basic_auth)
self.router = Router(ctx=ctx, url_prefix=opts.url_prefix, auth_controller=self.auth_controller)
for module in ('content', 'ajax', 'code', 'legacy'):
for module in ('content', 'ajax', 'code', 'legacy', 'opds'):
module = import_module('calibre.srv.' + module)
self.router.load_routes(vars(module).itervalues())
self.router.finalize()

View File

@ -11,9 +11,12 @@ from calibre.srv.routes import endpoint
@endpoint('/browse/{+rest=""}')
def browse(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for('') or '/')
raise HTTPRedirect(ctx.url_for(None))
@endpoint('/mobile/{+rest=""}')
def mobile(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for('') or '/')
raise HTTPRedirect(ctx.url_for(None))
@endpoint('/stanza/{+rest=""}')
def stanza(ctx, rd, rest):
raise HTTPRedirect(ctx.url_for('/opds'))

592
src/calibre/srv/opds.py Normal file
View File

@ -0,0 +1,592 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import hashlib, binascii
from functools import partial
from itertools import repeat
from collections import OrderedDict, namedtuple
from urllib import urlencode
from lxml import etree, html
from lxml.builder import ElementMaker
from calibre.constants import __appname__
from calibre.ebooks.metadata import fmt_sidx, authors_to_string
from calibre.library.comments import comments_to_html
from calibre import guess_type, prepare_string_for_xml as xml
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key
from calibre.utils.date import as_utc, timestampfromdt
from calibre.srv.errors import HTTPNotFound
from calibre.srv.routes import endpoint
from calibre.srv.utils import get_library_data, http_date, Offsets
def hexlify(x):
if isinstance(x, unicode):
x = x.encode('utf-8')
return binascii.hexlify(x)
def unhexlify(x):
return binascii.unhexlify(x).decode('utf-8')
def atom(ctx, rd, endpoint, output):
rd.outheaders.set('Content-Type', 'application/atom+xml; charset=UTF-8', replace_all=True)
if isinstance(output, bytes):
ans = output # Assume output is already UTF-8 XML
elif isinstance(output, type('')):
ans = output.encode('utf-8')
else:
from lxml import etree
ans = etree.tostring(output, encoding='utf-8', xml_declaration=True, pretty_print=True)
return ans
def format_tag_string(tags, sep, no_tag_count=False, joinval=', '):
if tags:
tlist = tags if sep is None else [t.strip() for t in tags.split(sep)]
else:
tlist = []
tlist.sort(key=sort_key)
if no_tag_count:
return joinval.join(tlist) if tlist else ''
else:
return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
joinval.join(tlist)) if tlist else ''
# Vocabulary for building OPDS feeds {{{
E = ElementMaker(namespace='http://www.w3.org/2005/Atom',
nsmap={
None : 'http://www.w3.org/2005/Atom',
'dc' : 'http://purl.org/dc/terms/',
'opds' : 'http://opds-spec.org/2010/catalog',
})
FEED = E.feed
TITLE = E.title
ID = E.id
ICON = E.icon
def UPDATED(dt, *args, **kwargs):
return E.updated(as_utc(dt).strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs)
LINK = partial(E.link, type='application/atom+xml')
NAVLINK = partial(E.link,
type='application/atom+xml;type=feed;profile=opds-catalog')
def SEARCH_LINK(url_for, *args, **kwargs):
kwargs['rel'] = 'search'
kwargs['title'] = 'Search'
kwargs['href'] = url_for('/opds/search', query='XXX').replace('XXX', '{searchTerms}')
return LINK(*args, **kwargs)
def AUTHOR(name, uri=None):
args = [E.name(name)]
if uri is not None:
args.append(E.uri(uri))
return E.author(*args)
SUBTITLE = E.subtitle
def NAVCATALOG_ENTRY(url_for, updated, title, description, query):
href = url_for('/opds/navcatalog', which=hexlify(query))
id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest())
return E.entry(
TITLE(title),
ID(id_),
UPDATED(updated),
E.content(description, type='text'),
NAVLINK(href=href)
)
START_LINK = partial(NAVLINK, rel='start')
UP_LINK = partial(NAVLINK, rel='up')
FIRST_LINK = partial(NAVLINK, rel='first')
LAST_LINK = partial(NAVLINK, rel='last')
NEXT_LINK = partial(NAVLINK, rel='next', title='Next')
PREVIOUS_LINK = partial(NAVLINK, rel='previous')
def html_to_lxml(raw):
raw = u'<div>%s</div>'%raw
root = html.fragment_fromstring(raw)
root.set('xmlns', "http://www.w3.org/1999/xhtml")
raw = etree.tostring(root, encoding=None)
try:
return etree.fromstring(raw)
except:
for x in root.iterdescendants():
remove = []
for attr in x.attrib:
if ':' in attr:
remove.append(attr)
for a in remove:
del x.attrib[a]
raw = etree.tostring(root, encoding=None)
try:
return etree.fromstring(raw)
except:
from calibre.ebooks.oeb.parse_utils import _html4_parse
return _html4_parse(raw)
def CATALOG_ENTRY(item, item_kind, request_context, updated, catalog_name,
ignore_count=False, add_kind=False):
id_ = 'calibre:category:'+item.name
iid = 'N' + item.name
if item.id is not None:
iid = 'I' + str(item.id)
iid += ':'+item_kind
href = request_context.url_for('/opds/category', category=hexlify(catalog_name), which=hexlify(iid))
link = NAVLINK(href=href)
count = (_('%d books') if item.count > 1 else _('%d book'))%item.count
if ignore_count:
count = ''
if item.use_sort_as_name:
name = item.sort
else:
name = item.name
return E.entry(
TITLE(name + ('' if not add_kind else ' (%s)'%item_kind)),
ID(id_),
UPDATED(updated),
E.content(count, type='text'),
link
)
def CATALOG_GROUP_ENTRY(item, category, request_context, updated):
id_ = 'calibre:category-group:'+category+':'+item.text
iid = item.text
link = NAVLINK(href=request_context.url_for('/opds/categorygroup', category=hexlify(category), which=hexlify(iid)))
return E.entry(
TITLE(item.text),
ID(id_),
UPDATED(updated),
E.content(_('%d items')%item.count, type='text'),
link
)
def ACQUISITION_ENTRY(book_id, updated, request_context):
field_metadata = request_context.db.field_metadata
mi = request_context.db.get_metadata(book_id)
extra = []
if mi.rating > 0:
rating = u''.join(repeat(u'\u2605', int(mi.rating/2.)))
extra.append(_('RATING: %s<br />')%rating)
if mi.tags:
extra.append(_('TAGS: %s<br />')%xml(format_tag_string(mi.tags, None, no_tag_count=True)))
if mi.series:
extra.append(_('SERIES: %(series)s [%(sidx)s]<br />')%
dict(series=xml(mi.series),
sidx=fmt_sidx(float(mi.series_index))))
for key in field_metadata.ignorable_field_keys():
name, val = mi.format_field(key)
if val:
fm = field_metadata[key]
datatype = fm['datatype']
if datatype == 'text' and fm['is_multiple']:
extra.append('%s: %s<br />'%
(xml(name),
xml(format_tag_string(val,
fm['is_multiple']['ui_to_list'],
no_tag_count=True,
joinval=fm['is_multiple']['list_to_ui']))))
elif datatype == 'comments' or (fm['datatype'] == 'composite' and
fm['display'].get('contains_html', False)):
extra.append('%s: %s<br />'%(xml(name), comments_to_html(unicode(val))))
else:
extra.append('%s: %s<br />'%(xml(name), xml(unicode(val))))
if mi.comments:
comments = comments_to_html(mi.comments)
extra.append(comments)
if extra:
extra = html_to_lxml('\n'.join(extra))
ans = E.entry(TITLE(mi.title), E.author(E.name(authors_to_string(mi.authors))), ID('urn:uuid:' + mi.uuid), UPDATED(updated))
if len(extra):
ans.append(E.content(extra, type='xhtml'))
get = partial(request_context.ctx.url_for, '/get', book_id=book_id, library_id=request_context.library_id)
if mi.formats:
for fmt in mi.formats:
fmt = fmt.lower()
mt = guess_type('a.'+fmt)[0]
if mt:
ans.append(E.link(type=mt, href=get(what=fmt), rel="http://opds-spec.org/acquisition"))
ans.append(E.link(type='image/jpeg', href=get(what='cover'), rel="http://opds-spec.org/cover"))
ans.append(E.link(type='image/jpeg', href=get(what='thumb'), rel="http://opds-spec.org/thumbnail"))
return ans
# }}}
default_feed_title = __appname__ + ' ' + _('Library')
class Feed(object): # {{{
def __init__(self, id_, updated, request_context, subtitle=None,
title=None,
up_link=None, first_link=None, last_link=None,
next_link=None, previous_link=None):
self.base_href = request_context.url_for('/opds')
self.root = \
FEED(
TITLE(title or default_feed_title),
AUTHOR(__appname__, uri='http://calibre-ebook.com'),
ID(id_),
ICON(request_context.ctx.url_for('/favicon.png')),
UPDATED(updated),
SEARCH_LINK(request_context.url_for),
START_LINK(href=request_context.url_for('/opds'))
)
if up_link:
self.root.append(UP_LINK(href=up_link))
if first_link:
self.root.append(FIRST_LINK(href=first_link))
if last_link:
self.root.append(LAST_LINK(href=last_link))
if next_link:
self.root.append(NEXT_LINK(href=next_link))
if previous_link:
self.root.append(PREVIOUS_LINK(href=previous_link))
if subtitle:
self.root.insert(1, SUBTITLE(subtitle))
# }}}
class TopLevel(Feed): # {{{
def __init__(self,
updated, # datetime object in UTC
categories,
request_context,
id_='urn:calibre:main',
subtitle=_('Books in your library')
):
Feed.__init__(self, id_, updated, request_context, subtitle=subtitle)
subc = partial(NAVCATALOG_ENTRY, request_context.url_for, updated)
subcatalogs = [subc(_('By ')+title,
_('Books sorted by ') + desc, q) for title, desc, q in
categories]
for x in subcatalogs:
self.root.append(x)
for library_id, library_name in request_context.library_map.iteritems():
id_ = 'calibre-library:' + library_id
self.root.append(E.entry(
TITLE(_('Library:') + ' ' + library_name),
ID(id_),
UPDATED(updated),
E.content(_('Change calibre library to:') + ' ' + library_name, type='text'),
NAVLINK(href=request_context.url_for('/opds', library_id=library_id))
))
# }}}
class NavFeed(Feed):
def __init__(self, id_, updated, request_context, offsets, page_url, up_url, title=None):
kwargs = {'up_link': up_url}
kwargs['first_link'] = page_url
kwargs['last_link'] = page_url+'?offset=%d'%offsets.last_offset
if offsets.offset > 0:
kwargs['previous_link'] = \
page_url+'?offset=%d'%offsets.previous_offset
if offsets.next_offset > -1:
kwargs['next_link'] = \
page_url+'?offset=%d'%offsets.next_offset
if title:
kwargs['title'] = title
Feed.__init__(self, id_, updated, request_context, **kwargs)
class AcquisitionFeed(NavFeed):
def __init__(self, id_, updated, request_context, items, offsets, page_url, up_url, title=None):
NavFeed.__init__(self, id_, updated, request_context, offsets, page_url, up_url, title=title)
for book_id in items:
self.root.append(ACQUISITION_ENTRY(book_id, updated, request_context))
class CategoryFeed(NavFeed):
def __init__(self, items, which, id_, updated, request_context, offsets, page_url, up_url, title=None):
NavFeed.__init__(self, id_, updated, request_context, offsets, page_url, up_url, title=title)
ignore_count = False
if which == 'search':
ignore_count = True
for item in items:
self.root.append(CATALOG_ENTRY(
item, item.category, request_context, updated, which, ignore_count=ignore_count, add_kind=which != item.category))
class CategoryGroupFeed(NavFeed):
def __init__(self, items, which, id_, updated, request_context, offsets, page_url, up_url, title=None):
NavFeed.__init__(self, id_, updated, request_context, offsets, page_url, up_url, title=title)
for item in items:
self.root.append(CATALOG_GROUP_ENTRY(item, which, request_context, updated))
class RequestContext(object):
def __init__(self, ctx, rd):
self.db, self.library_id, self.library_map, self.default_library = get_library_data(ctx, rd.query)
self.ctx, self.rd = ctx, rd
def url_for(self, path, **kwargs):
lid = kwargs.pop('library_id', self.library_id)
ans = self.ctx.url_for(path, **kwargs)
q = {'library_id':lid}
ans += '?' + urlencode(q)
return ans
def allowed_book_ids(self):
return self.ctx.allowed_book_ids(self.rd, self.db)
@property
def outheaders(self):
return self.rd.outheaders
@property
def opts(self):
return self.ctx.opts
def last_modified(self):
return self.db.last_modified()
def get_categories(self):
return self.ctx.get_categories(self.rd, self.db)
def search(self, query):
return self.ctx.search(self.rd, self.db, query)
def get_acquisition_feed(rc, ids, offset, page_url, up_url, id_,
sort_by='title', ascending=True, feed_title=None):
if not ids:
raise HTTPNotFound('No books found')
with rc.db.safe_read_lock:
items = rc.db.multisort([(sort_by, ascending)], ids)
max_items = rc.opts.max_opds_items
offsets = Offsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.offset+max_items]
lm = rc.last_modified()
rc.outheaders['Last-Modified'] = http_date(timestampfromdt(lm))
return AcquisitionFeed(id_, lm, rc, items, offsets, page_url, up_url, title=feed_title).root
def get_all_books(rc, which, page_url, up_url, offset=0):
try:
offset = int(offset)
except Exception:
raise HTTPNotFound('Not found')
if which not in ('title', 'newest'):
raise HTTPNotFound('Not found')
sort = 'timestamp' if which == 'newest' else 'title'
ascending = which == 'title'
feed_title = {'newest':_('Newest'), 'title': _('Title')}.get(which, which)
feed_title = default_feed_title + ' :: ' + _('By %s') % feed_title
ids = rc.allowed_book_ids()
return get_acquisition_feed(rc, ids, offset, page_url, up_url,
id_='calibre-all:'+sort, sort_by=sort, ascending=ascending,
feed_title=feed_title)
def get_navcatalog(request_context, which, page_url, up_url, offset=0):
categories = request_context.get_categories()
if which not in categories:
raise HTTPNotFound('Category %r not found'%which)
items = categories[which]
updated = request_context.last_modified()
category_meta = request_context.db.field_metadata
meta = category_meta.get(which, {})
category_name = meta.get('name', which)
feed_title = default_feed_title + ' :: ' + _('By %s') % category_name
id_ = 'calibre-category-feed:'+which
MAX_ITEMS = request_context.opts.max_opds_ungrouped_items
if MAX_ITEMS > 0 and len(items) <= MAX_ITEMS:
max_items = request_context.opts.max_opds_items
offsets = Offsets(offset, max_items, len(items))
items = list(items)[offsets.offset:offsets.offset+max_items]
ans = CategoryFeed(items, which, id_, updated, request_context, offsets,
page_url, up_url, title=feed_title)
else:
Group = namedtuple('Group', 'text count')
starts = set()
for x in items:
val = getattr(x, 'sort', x.name)
if not val:
val = 'A'
starts.add(val[0].upper())
category_groups = OrderedDict()
for x in sorted(starts, key=sort_key):
category_groups[x] = len([y for y in items if
getattr(y, 'sort', y.name).startswith(x)])
items = [Group(x, y) for x, y in category_groups.items()]
max_items = request_context.opts.max_opds_items
offsets = Offsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.offset+max_items]
ans = CategoryGroupFeed(items, which, id_, updated, request_context, offsets,
page_url, up_url, title=feed_title)
request_context.outheaders['Last-Modified'] = http_date(timestampfromdt(updated))
return ans.root
@endpoint('/opds', postprocess=atom)
def opds(ctx, rd):
rc = RequestContext(ctx, rd)
db = rc.db
categories = rc.get_categories()
category_meta = db.field_metadata
cats = [
(_('Newest'), _('Date'), 'Onewest'),
(_('Title'), _('Title'), 'Otitle'),
]
def getter(x):
try:
return category_meta[x]['name'].lower()
except KeyError:
return x
for category in sorted(categories, key=lambda x: sort_key(getter(x))):
if len(categories[category]) == 0:
continue
if category in ('formats', 'identifiers'):
continue
meta = category_meta.get(category, None)
if meta is None:
continue
cats.append((meta['name'], meta['name'], 'N'+category))
last_modified = db.last_modified()
rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified))
return TopLevel(last_modified, cats, rc).root
@endpoint('/opds/navcatalog/{which}', postprocess=atom)
def opds_navcatalog(ctx, rd, which):
try:
offset = int(rd.query.get('offset', 0))
except Exception:
raise HTTPNotFound('Not found')
rc = RequestContext(ctx, rd)
page_url = rc.url_for('/opds/navcatalog', which=which)
up_url = rc.url_for('/opds')
which = unhexlify(which)
type_ = which[0]
which = which[1:]
if type_ == 'O':
return get_all_books(rc, which, page_url, up_url, offset=offset)
elif type_ == 'N':
return get_navcatalog(rc, which, page_url, up_url, offset=offset)
raise HTTPNotFound('Not found')
@endpoint('/opds/category/{category}/{which}', postprocess=atom)
def opds_category(ctx, rd, category, which):
try:
offset = int(rd.query.get('offset', 0))
except Exception:
raise HTTPNotFound('Not found')
if not which or not category:
raise HTTPNotFound('Not found')
rc = RequestContext(ctx, rd)
page_url = rc.url_for('/opds/category', which=which, category=category)
up_url = rc.url_for('/opds/navcatalog', which=category)
which, category = unhexlify(which), unhexlify(category)
type_ = which[0]
which = which[1:]
if type_ == 'I':
try:
p = which.rindex(':')
category = which[p+1:]
which = which[:p]
# This line will toss an exception for composite columns
which = int(which[:p])
except Exception:
# Might be a composite column, where we have the lookup key
if not (category in rc.db.field_metadata and
rc.db.field_metadata[category]['datatype'] == 'composite'):
raise HTTPNotFound('Tag %r not found'%which)
categories = rc.get_categories()
if category not in categories:
raise HTTPNotFound('Category %r not found'%which)
if category == 'search':
try:
ids = rc.search('search:"%s"'%which)
except Exception:
raise HTTPNotFound('Search: %r not understood'%which)
return get_acquisition_feed(rc, ids, offset, page_url, up_url, 'calibre-search:'+which)
if type_ != 'I':
raise HTTPNotFound('Non id categories not supported')
q = category
if q == 'news':
q = 'tags'
ids = rc.db.get_books_for_category(q, which)
sort_by = 'series' if category == 'series' else 'title'
return get_acquisition_feed(rc, ids, offset, page_url, up_url, 'calibre-category:'+category+':'+str(which), sort_by=sort_by)
@endpoint('/opds/categorygroup/{category}/{which}', postprocess=atom)
def opds_categorygroup(ctx, rd, category, which):
try:
offset = int(rd.query.get('offset', 0))
except Exception:
raise HTTPNotFound('Not found')
if not which or not category:
raise HTTPNotFound('Not found')
rc = RequestContext(ctx, rd)
categories = rc.get_categories()
page_url = rc.url_for('/opds/categorygroup', category=category, which=which)
category = unhexlify(category)
if category not in categories:
raise HTTPNotFound('Category %r not found'%which)
category_meta = rc.db.field_metadata
meta = category_meta.get(category, {})
category_name = meta.get('name', which)
which = unhexlify(which)
feed_title = default_feed_title + ' :: ' + (_('By {0} :: {1}').format(category_name, which))
owhich = hexlify('N'+which)
up_url = rc.url_for('/opds/navcatalog', which=owhich)
items = categories[category]
def belongs(x, which):
return getattr(x, 'sort', x.name).lower().startswith(which.lower())
items = [x for x in items if belongs(x, which)]
if not items:
raise HTTPNotFound('No items in group %r:%r'%(category, which))
updated = rc.last_modified()
id_ = 'calibre-category-group-feed:'+category+':'+which
max_items = rc.opts.max_opds_items
offsets = Offsets(offset, max_items, len(items))
items = list(items)[offsets.offset:offsets.offset+max_items]
rc.outheaders['Last-Modified'] = http_date(timestampfromdt(updated))
return CategoryFeed(items, category, id_, updated, rc, offsets, page_url, up_url, title=feed_title).root
@endpoint('/opds/search/{query=""}', postprocess=atom)
def opds_search(ctx, rd, query):
try:
offset = int(rd.query.get('offset', 0))
except Exception:
raise HTTPNotFound('Not found')
rc = RequestContext(ctx, rd)
try:
ids = rc.search(query)
except Exception:
raise HTTPNotFound('Search: %r not understood'%query)
page_url = rc.url_for('/opds/search', query=query)
return get_acquisition_feed(rc, ids, offset, page_url, rc.url_for('/opds'), 'calibre-search:'+query)

View File

@ -70,6 +70,16 @@ raw_options = (
'Advertise the OPDS feeds via the BonJour service, so that OPDS based'
' reading apps can detect and connect to the server automatically.',
'Maximum number of books in OPDS feeds',
'max_opds_items', 30,
'The maximum number of books that the server will return in a single'
' OPDS acquisition feed.',
'Maximum number of ungrouped items in OPDS feeds',
'max_opds_ungrouped_items', 100,
'Group items in categories such as author/tags by first letter when'
' there are more than this number of items. Set to zero to disable.',
'The interface on which to listen for connections',
'listen_on', '0.0.0.0',
'The default is to listen on all available interfaces. You can change this to, for'

View File

@ -19,6 +19,7 @@ from binascii import hexlify, unhexlify
from calibre import prints
from calibre.constants import iswindows
from calibre.srv.errors import HTTPNotFound
from calibre.utils.config_base import tweaks
from calibre.utils.localization import get_translator
from calibre.utils.socket_inheritance import set_socket_inherit
@ -469,3 +470,38 @@ class ReadOnlyFileBuffer(object):
def close(self):
pass
def get_db(ctx, library_id):
db = ctx.get_library(library_id)
if db is None:
raise HTTPNotFound('Library %r not found' % library_id)
return db
def get_library_data(ctx, query):
library_id = query.get('library_id')
library_map, default_library = ctx.library_map
if library_id not in library_map:
library_id = default_library
db = get_db(ctx, library_id)
return db, library_id, library_map, default_library
class Offsets(object):
'Calculate offsets for a paginated view'
def __init__(self, offset, delta, total):
if offset < 0:
offset = 0
if offset >= total:
raise HTTPNotFound('Invalid offset: %r'%offset)
last_allowed_index = total - 1
last_current_index = offset + delta - 1
self.slice_upper_bound = offset+delta
self.offset = offset
self.next_offset = last_current_index + 1
if self.next_offset > last_allowed_index:
self.next_offset = -1
self.previous_offset = self.offset - delta
if self.previous_offset < 0:
self.previous_offset = 0
self.last_offset = last_allowed_index - delta
if self.last_offset < 0:
self.last_offset = 0