diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 17853b818f..5e6c10c27b 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -626,20 +626,24 @@ class ResultCache(SearchQueryParser): self._map.sort(cmp=fcmp, reverse=not ascending) self._map_filtered = [id for id in self._map if id in self._map_filtered] - def search(self, query, return_matches=False): + def search(self, query, return_matches=False, + ignore_search_restriction=False): if not query or not query.strip(): - q = self.search_restriction - else: - q = '%s (%s)' % (self.search_restriction, query) + q = '' + if not ignore_search_restriction: + q = self.search_restriction + elif not ignore_search_restriction: + q = u'%s (%s)' % (self.search_restriction, query) if not q: if return_matches: - return list(self.map) # when return_matches, do not update the maps! + return list(self._map) # when return_matches, do not update the maps! self._map_filtered = list(self._map) return [] matches = sorted(self.parse(q)) + ans = [id for id in self._map if id in matches] if return_matches: - return [id for id in self._map if id in matches] - self._map_filtered = [id for id in self._map if id in matches] + return ans + self._map_filtered = ans return [] def set_search_restriction(self, s): diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py index 89dc140434..5c9be367d0 100644 --- a/src/calibre/library/server/cache.py +++ b/src/calibre/library/server/cache.py @@ -6,13 +6,28 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' from calibre.utils.date import utcnow +from calibre.utils.ordered_dict import OrderedDict class Cache(object): - @property - def categories_cache(self): - old = getattr(self, '_category_cache', None) + def add_routes(self, c): + self._category_cache = OrderedDict() + self._search_cache = OrderedDict() + + def search_cache(self, search): + old = self._search_cache.get(search, None) if old is None or old[0] <= self.db.last_modified(): - categories = self.db.get_categories() - self._category_cache = (utcnow(), categories) - return self._category_cache[1] + matches = self.db.data.search(search) + self._search_cache[search] = frozenset(matches) + if len(self._search_cache) > 10: + self._search_cache.popitem(last=False) + + + def categories_cache(self, restrict_to=frozenset([])): + old = self._category_cache.get(frozenset(restrict_to), None) + if old is None or old[0] <= self.db.last_modified(): + categories = self.db.get_categories(ids=restrict_to) + self._category_cache[restrict_to] = (utcnow(), categories) + if len(self._category_cache) > 10: + self._category_cache.popitem(last=False) + return self._category_cache[restrict_to][1] diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index 359449a838..23ee58da7f 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -5,20 +5,20 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, hashlib -from itertools import repeat +import hashlib, binascii from functools import partial -import cherrypy from lxml import etree from lxml.builder import ElementMaker +import cherrypy -from calibre.utils.genshi.template import MarkupTemplate -from calibre.library.server.utils import strftime, expose -from calibre.ebooks.metadata import fmt_sidx, title_sort -from calibre import guess_type, prepare_string_for_xml from calibre.constants import __appname__ +BASE_HREFS = { + 0 : '/stanza', + 1 : '/opds', +} + # Vocabulary for building OPDS feeds {{{ E = ElementMaker(namespace='http://www.w3.org/2005/Atom', nsmap={ @@ -42,7 +42,7 @@ NAVLINK = partial(E.link, def SEARCH(base_href, *args, **kwargs): kwargs['rel'] = 'search' kwargs['title'] = 'Search' - kwargs['href'] = base_href+'/?search={searchTerms}' + kwargs['href'] = base_href+'/search/{searchTerms}' return LINK(*args, **kwargs) def AUTHOR(name, uri=None): @@ -53,11 +53,9 @@ def AUTHOR(name, uri=None): SUBTITLE = E.subtitle -def NAVCATALOG_ENTRY(base_href, updated, title, description, query_data): - data = [u'%s=%s'%(key, val) for key, val in query_data.items()] - data = '&'.join(data) - href = base_href+'/?'+data - id_ = 'calibre-subcatalog:'+str(hashlib.sha1(href).hexdigest()) +def NAVCATALOG_ENTRY(base_href, updated, title, description, query): + href = base_href+'/navcatalog/'+binascii.hexlify(query) + id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest()) return E.entry( TITLE(title), ID(id_), @@ -79,14 +77,15 @@ class TopLevel(Feed): def __init__(self, updated, # datetime object in UTC categories, + version, id_ = 'urn:calibre:main', - base_href = '/stanza' ): + base_href = BASE_HREFS[version] self.base_href = base_href subc = partial(NAVCATALOG_ENTRY, base_href, updated) - subcatalogs = [subc('By '+title, - 'Books sorted by '+desc, {'sortby':q}) for title, desc, q in + subcatalogs = [subc(_('By ')+title, + _('Books sorted by ') + desc, q) for title, desc, q in categories] self.root = \ @@ -100,248 +99,69 @@ class TopLevel(Feed): *subcatalogs ) - - -# Templates {{{ - -STANZA_ENTRY=MarkupTemplate('''\ - - ${record[FM['title']]} - urn:calibre:${urn} - ${authors} - ${timestamp} - - - - -
${Markup(extra)}${record[FM['comments']]}
-
-
-''') - -STANZA_SUBCATALOG_ENTRY=MarkupTemplate('''\ - - ${title} - urn:calibre:${id} - ${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')} - - ${count} books - -''') - -# Feed of books -STANZA = MarkupTemplate('''\ - - - calibre Library - $id - ${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')} - - ${Markup(next_link)} - - calibre - http://calibre-ebook.com - - - ${subtitle} - - - ${Markup(entry)} - - -''') - - -# }}} +STANZA_FORMATS = frozenset(['epub', 'pdb']) class OPDSServer(object): - def build_top_level(self, updated, base_href='/stanza'): - categories = self.categories_cache - categories = [(x.capitalize(), x.capitalize(), x) for x in - categories.keys()] - categories.append(('Title', 'Title', '|title|')) - categories.append(('Newest', 'Newest', '|newest|')) + def add_routes(self, connect): + for base in ('stanza', 'opds'): + version = 0 if base == 'stanza' else 1 + base_href = BASE_HREFS[version] + connect(base, base_href, self.opds, version=version) + connect('opdsnavcatalog_'+base, base_href+'/navcatalog/{which}', + self.opds_navcatalog, version=version) + connect('opdssearch_'+base, base_href+'/search/{terms}', + self.opds_search, version=version) - return TopLevel(updated, categories, base_href=base_href) + def get_opds_allowed_ids_for_version(self, version): + search = '' if version > 0 else ' '.join(['format:='+x for x in + STANZA_FORMATS]) + self.seach_cache(search) - def get_matches(self, location, query): - base = self.db.data.get_matches(location, query) - epub = self.db.data.get_matches('format', '=epub') - pdb = self.db.data.get_matches('format', '=pdb') - return base.intersection(epub.union(pdb)) + def opds_search(self, terms=None, version=0): + version = int(version) + if not terms or version not in BASE_HREFS: + raise cherrypy.HTTPError(404, 'Not found') - def stanza_sortby_subcategory(self, updated, sortby, offset): - pat = re.compile(r'\(.*\)') + def opds_navcatalog(self, which=None, version=0): + version = int(version) + if not which or version not in BASE_HREFS: + raise cherrypy.HTTPError(404, 'Not found') + which = binascii.unhexlify(which) + type_ = which[0] + which = which[1:] + if type_ == 'O': + return self.get_opds_all_books(which) + elif type_ == 'N': + return self.get_opds_navcatalog(which) + raise cherrypy.HTTPError(404, 'Not found') - def clean_author(x): - return pat.sub('', x).strip() - - def author_cmp(x, y): - x = x if ',' in x else clean_author(x).rpartition(' ')[-1] - y = y if ',' in y else clean_author(y).rpartition(' ')[-1] - return cmp(x.lower(), y.lower()) - - def get_author(x): - pref, ___, suff = clean_author(x).rpartition(' ') - return suff + (', '+pref) if pref else suff - - - what, subtitle = sortby[2:], '' - if sortby == 'byseries': - data = self.db.all_series() - data = [(x[0], x[1], len(self.get_matches('series', '='+x[1]))) for x in data] - subtitle = 'Books by series' - elif sortby == 'byauthor': - data = self.db.all_authors() - data = [(x[0], x[1], len(self.get_matches('authors', '='+x[1]))) for x in data] - subtitle = 'Books by author' - elif sortby == 'bytag': - data = self.db.all_tags2() - data = [(x[0], x[1], len(self.get_matches('tags', '='+x[1]))) for x in data] - subtitle = 'Books by tag' - fcmp = author_cmp if sortby == 'byauthor' else cmp - data = [x for x in data if x[2] > 0] - data.sort(cmp=lambda x, y: fcmp(x[1], y[1])) - next_offset = offset + self.max_stanza_items - rdata = data[offset:next_offset] - if next_offset >= len(data): - next_offset = -1 - gt = get_author if sortby == 'byauthor' else lambda x: x - entries = [STANZA_SUBCATALOG_ENTRY.generate(title=gt(title), id=id, - what=what, updated=updated, count=c).render('xml').decode('utf-8') for id, - title, c in rdata] - next_link = '' - if next_offset > -1: - next_link = ('\n' - ) % (sortby, next_offset) - return STANZA.generate(subtitle=subtitle, data=entries, FM=self.db.FIELD_MAP, - updated=updated, id='urn:calibre:main', next_link=next_link).render('xml') - - @expose - def stanza(self, search=None, sortby=None, authorid=None, tagid=None, - seriesid=None, offset=0): - 'Feeds to read calibre books on a ipod with stanza.' - books = [] + def opds(self, version=0): + version = int(version) + if version not in BASE_HREFS: + raise cherrypy.HTTPError(404, 'Not found') + categories = self.categories_cache( + self.get_opds_allowed_ids_for_version(version)) + category_meta = self.db.get_tag_browser_categories() + cats = [ + (_('Newest'), _('Date'), 'Onewest'), + (_('Title'), _('Title'), 'Otitle'), + ] + for category in categories: + if category == 'formats': + continue + meta = category_meta.get(category, None) + if meta is None: + continue + cats.append((meta['name'], meta['name'], 'N'+category)) updated = self.db.last_modified() - offset = int(offset) + cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Content-Type'] = 'text/xml' - # Top Level feed - if not sortby and not search and not authorid and not tagid and not seriesid: - return str(self.build_top_level(updated)) + feed = TopLevel(updated, cats, version) - if sortby in ('byseries', 'byauthor', 'bytag'): - return self.stanza_sortby_subcategory(updated, sortby, offset) - - # Get matching ids - if authorid: - authorid=int(authorid) - au = self.db.author_name(authorid) - ids = self.get_matches('authors', au) - elif tagid: - tagid=int(tagid) - ta = self.db.tag_name(tagid) - ids = self.get_matches('tags', ta) - elif seriesid: - seriesid=int(seriesid) - se = self.db.series_name(seriesid) - ids = self.get_matches('series', se) - else: - ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set() - record_list = list(iter(self.db)) - - FM = self.db.FIELD_MAP - # Sort the record list - if sortby == "bytitle" or authorid or tagid: - record_list.sort(lambda x, y: - cmp(title_sort(x[FM['title']]), - title_sort(y[FM['title']]))) - elif seriesid: - record_list.sort(lambda x, y: - cmp(x[FM['series_index']], - y[FM['series_index']])) - else: # Sort by date - record_list = reversed(record_list) + return str(feed) - fmts = FM['formats'] - pat = re.compile(r'EPUB|PDB', re.IGNORECASE) - record_list = [x for x in record_list if x[FM['id']] in ids and - pat.search(x[fmts] if x[fmts] else '') is not None] - next_offset = offset + self.max_stanza_items - nrecord_list = record_list[offset:next_offset] - if next_offset >= len(record_list): - next_offset = -1 - - next_link = '' - if next_offset > -1: - q = ['offset=%d'%next_offset] - for x in ('search', 'sortby', 'authorid', 'tagid', 'seriesid'): - val = locals()[x] - if val is not None: - val = prepare_string_for_xml(unicode(val), True) - q.append('%s=%s'%(x, val)) - next_link = ('\n' - ) % '&'.join(q) - - for record in nrecord_list: - r = record[FM['formats']] - r = r.upper() if r else '' - - z = record[FM['authors']] - if not z: - z = _('Unknown') - authors = ' & '.join([i.replace('|', ',') for i in - z.split(',')]) - - # Setup extra description - extra = [] - rating = record[FM['rating']] - if rating > 0: - rating = ''.join(repeat('★', rating)) - extra.append('RATING: %s
'%rating) - tags = record[FM['tags']] - if tags: - extra.append('TAGS: %s
'%\ - prepare_string_for_xml(', '.join(tags.split(',')))) - series = record[FM['series']] - if series: - extra.append('SERIES: %s [%s]
'%\ - (prepare_string_for_xml(series), - fmt_sidx(float(record[FM['series_index']])))) - - fmt = 'epub' if 'EPUB' in r else 'pdb' - mimetype = guess_type('dummy.'+fmt)[0] - - # Create the sub-catalog, which is either a list of - # authors/tags/series or a list of books - data = dict( - record=record, - updated=updated, - authors=authors, - tags=tags, - series=series, - FM=FM, - extra='\n'.join(extra), - mimetype=mimetype, - fmt=fmt, - urn=record[FM['uuid']], - timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', - record[FM['timestamp']]) - ) - books.append(STANZA_ENTRY.generate(**data)\ - .render('xml').decode('utf8')) - - return STANZA.generate(subtitle='', data=books, FM=FM, - next_link=next_link, updated=updated, id='urn:calibre:main').render('xml') - - -if __name__ == '__main__': - from datetime import datetime - f = TopLevel(datetime.utcnow()) - print f