diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py
index 5c9be367d0..f8de28a735 100644
--- a/src/calibre/library/server/cache.py
+++ b/src/calibre/library/server/cache.py
@@ -17,10 +17,14 @@ class Cache(object):
def search_cache(self, search):
old = self._search_cache.get(search, None)
if old is None or old[0] <= self.db.last_modified():
- matches = self.db.data.search(search)
- self._search_cache[search] = frozenset(matches)
+ matches = self.db.data.search(search, return_matches=True,
+ ignore_search_restriction=True)
+ if not matches:
+ matches = []
+ self._search_cache[search] = (utcnow(), frozenset(matches))
if len(self._search_cache) > 10:
self._search_cache.popitem(last=False)
+ return self._search_cache[search][1]
def categories_cache(self, restrict_to=frozenset([])):
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index d6702cbe75..149f12644c 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -7,18 +7,24 @@ __docformat__ = 'restructuredtext en'
import hashlib, binascii
from functools import partial
+from itertools import repeat
-from lxml import etree
+from lxml import etree, html
from lxml.builder import ElementMaker
import cherrypy
from calibre.constants import __appname__
+from calibre.ebooks.metadata import fmt_sidx
+from calibre.library.comments import comments_to_html
+from calibre import guess_type
BASE_HREFS = {
0 : '/stanza',
1 : '/opds',
}
+STANZA_FORMATS = frozenset(['epub', 'pdb'])
+
# Vocabulary for building OPDS feeds {{{
E = ElementMaker(namespace='http://www.w3.org/2005/Atom',
nsmap={
@@ -71,9 +77,72 @@ LAST_LINK = partial(NAVLINK, rel='last')
NEXT_LINK = partial(NAVLINK, rel='next')
PREVIOUS_LINK = partial(NAVLINK, rel='previous')
+def html_to_lxml(raw):
+ raw = u'
%s
'%raw
+ root = html.fragment_fromstring(raw)
+ root.set('xmlns', "http://www.w3.org/1999/xhtml")
+ raw = etree.tostring(root, encoding=None)
+ return etree.fromstring(raw)
+
+def ACQUISITION_ENTRY(item, version, FM, updated):
+ title = item[FM['title']]
+ if not title:
+ title = _('Unknown')
+ authors = item[FM['authors']]
+ if not authors:
+ authors = _('Unknown')
+ authors = ' & '.join([i.replace('|', ',') for i in
+ authors.split(',')])
+ extra = []
+ rating = item[FM['rating']]
+ if rating > 0:
+ rating = u''.join(repeat(u'\u2605', int(rating/2.)))
+ extra.append(_('RATING: %s
')%rating)
+ tags = item[FM['tags']]
+ if tags:
+ extra.append(_('TAGS: %s
')%\
+ ', '.join(tags.split(',')))
+ series = item[FM['series']]
+ if series:
+ extra.append(_('SERIES: %s [%s]
')%\
+ (series,
+ fmt_sidx(float(item[FM['series_index']]))))
+ comments = item[FM['comments']]
+ if comments:
+ comments = comments_to_html(comments)
+ extra.append(comments)
+ if extra:
+ extra = html_to_lxml('\n'.join(extra))
+ idm = 'calibre' if version == 0 else 'uuid'
+ id_ = 'urn:%s:%s'%(idm, item[FM['uuid']])
+ ans = E.entry(TITLE(title), E.author(E.name(authors)), ID(id_),
+ UPDATED(updated))
+ if extra:
+ ans.append(E.content(extra, type='xhtml'))
+ formats = item[FM['formats']]
+ if formats:
+ for fmt in formats.split(','):
+ fmt = fmt.lower()
+ mt = guess_type('a.'+fmt)[0]
+ href = '/get/%s/%s'%(fmt, item[FM['id']])
+ if mt:
+ link = E.link(type=mt, href=href)
+ if version > 0:
+ link.set('rel', "http://opds-spec.org/acquisition")
+ ans.append(link)
+ ans.append(E.link(type='image/jpeg', href='/get/cover/%s'%item[FM['id']],
+ rel="x-stanza-cover-image" if version == 0 else
+ "http://opds-spec.org/cover"))
+ ans.append(E.link(type='image/jpeg', href='/get/thumb/%s'%item[FM['id']],
+ rel="x-stanza-cover-image-thumbnail" if version == 0 else
+ "http://opds-spec.org/thumbnail"))
+
+ return ans
+
+
# }}}
-class Feed(object):
+class Feed(object): # {{{
def __init__(self, id_, updated, version, subtitle=None,
title=__appname__ + ' ' + _('Library'),
@@ -106,6 +175,7 @@ class Feed(object):
def __str__(self):
return etree.tostring(self.root, pretty_print=True, encoding='utf-8',
xml_declaration=True)
+ # }}}
class TopLevel(Feed): # {{{
@@ -126,9 +196,9 @@ class TopLevel(Feed): # {{{
self.root.append(x)
# }}}
-class AcquisitionFeed(Feed):
+class NavFeed(Feed):
- def __init__(self, updated, id_, items, offsets, page_url, up_url, version):
+ def __init__(self, id_, updated, version, offsets, page_url, up_url):
kwargs = {'up_link': up_url}
kwargs['first_link'] = page_url
kwargs['last_link'] = page_url+'?offset=%d'%offsets.last_offset
@@ -140,7 +210,14 @@ class AcquisitionFeed(Feed):
page_url+'?offset=%d'%offsets.next_offset
Feed.__init__(self, id_, updated, version, **kwargs)
-STANZA_FORMATS = frozenset(['epub', 'pdb'])
+class AcquisitionFeed(NavFeed):
+
+ def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
+ FM):
+ NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
+ for item in items:
+ self.root.append(ACQUISITION_ENTRY(item, version, FM, updated))
+
class OPDSOffsets(object):
@@ -176,9 +253,10 @@ class OPDSServer(object):
self.opds_search, version=version)
def get_opds_allowed_ids_for_version(self, version):
- search = '' if version > 0 else ' '.join(['format:='+x for x in
+ search = '' if version > 0 else ' or '.join(['format:='+x for x in
STANZA_FORMATS])
- self.search_cache(search)
+ ids = self.search_cache(search)
+ return ids
def get_opds_acquisition_feed(self, ids, offset, page_url, up_url, id_,
sort_by='title', ascending=True, version=0):
@@ -189,7 +267,8 @@ class OPDSServer(object):
max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.next_offset]
- return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets, page_url, up_url, version))
+ return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets,
+ page_url, up_url, version, self.db.FIELD_MAP))
def opds_search(self, query=None, version=0, offset=0):
try:
@@ -203,8 +282,9 @@ class OPDSServer(object):
ids = self.search_cache(query)
except:
raise cherrypy.HTTPError(404, 'Search: %r not understood'%query)
- return self.get_opds_acquisition_feed(ids,
- sort_by='title', version=version)
+ return self.get_opds_acquisition_feed(ids, offset, '/search/'+query,
+ BASE_HREFS[version], 'calibre-search:'+query,
+ version=version)
def opds_navcatalog(self, which=None, version=0):
version = int(version)