class AjaxServer(object): def __init__(self): self.ajax_json_codec = JsonCodec() def add_routes(self, connect): base_href = '/ajax' # Metadata for books connect('ajax_book', base_href + '/book/{book_id}', self.ajax_book) connect('ajax_books', base_href + '/books', self.ajax_books) # The list of top level categories connect('ajax_categories', base_href + '/categories', self.ajax_categories) # The list of sub-categories and items in each category connect('ajax_category', base_href + '/category/{name}', self.ajax_category) # List of books in specified category connect('ajax_books_in', base_href + '/books_in/{category}/{item}', self.ajax_books_in) # Search connect('ajax_search', base_href + '/search', self.ajax_search) # Get book metadata {{{ def ajax_book_to_json(self, book_id, get_category_urls=True, device_compatible=False, device_for_template=None): mi = self.db.get_metadata(book_id, index_is_id=True) if not device_compatible: try: mi.rating = mi.rating / 2. except: mi.rating = 0.0 data = self.ajax_json_codec.encode_book_metadata(mi) for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime', 'rights', 'book_producer'): data.pop(x, None) data['cover'] = absurl(self.opts.url_prefix, u'/get/cover/%d' % book_id) data['thumbnail'] = absurl(self.opts.url_prefix, u'/get/thumb/%d' % book_id) if not device_compatible: mi.format_metadata = { k.lower(): dict(v) for k, v in mi.format_metadata.iteritems() } for v in mi.format_metadata.itervalues(): mtime = v.get('mtime', None) if mtime is not None: v['mtime'] = isoformat(mtime, as_utc=True) data['format_metadata'] = mi.format_metadata fmts = set(x.lower() for x in mi.format_metadata.iterkeys()) pf = prefs['output_format'].lower() other_fmts = list(fmts) try: fmt = pf if pf in fmts else other_fmts[0] except: fmt = None if fmts and fmt: other_fmts = [x for x in fmts if x != fmt] data['formats'] = sorted(fmts) if fmt: data['main_format'] = { fmt: absurl(self.opts.url_prefix, u'/get/%s/%d' % (fmt, book_id)) } else: data['main_format'] = None data['other_formats'] = { fmt: absurl(self.opts.url_prefix, u'/get/%s/%d' % (fmt, book_id)) for fmt in other_fmts } if get_category_urls: category_urls = data['category_urls'] = {} ccache = self.categories_cache() for key in mi.all_field_keys(): fm = mi.metadata_for_field(key) if (fm and fm['is_category'] and not fm['is_csp'] and key != 'formats' and fm['datatype'] not in ['rating']): categories = mi.get(key) if isinstance(categories, basestring): categories = [categories] if categories is None: categories = [] dbtags = {} for category in categories: for tag in ccache.get(key, []): if tag.original_name == category: dbtags[category] = books_in_url( self.opts.url_prefix, tag.category if tag.category else key, tag.original_name if tag.id is None else unicode(tag.id)) break category_urls[key] = dbtags else: series = data.get('series', None) if series: tsorder = tweaks['save_template_title_series_sorting'] series = title_sort(series, order=tsorder) else: series = '' data['_series_sort_'] = series if device_for_template: import posixpath from calibre.devices.utils import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize from calibre.customize.ui import device_plugins for device_class in device_plugins(): if device_class.__class__.__name__ == device_for_template: template = device_class.save_template() data['_filename_'] = create_upload_path( mi, book_id, template, sanitize, path_type=posixpath) break return data, mi.last_modified @Endpoint(set_last_modified=False) def ajax_book(self, book_id, category_urls='true', id_is_uuid='false', device_compatible='false', device_for_template=None): ''' Return the metadata of the book as a JSON dictionary. If category_urls == 'true' the returned dictionary also contains a mapping of category names to URLs that return the list of books in the given category. ''' cherrypy.response.timeout = 3600 try: if id_is_uuid == 'true': book_id = self.db.get_id_from_uuid(book_id) else: book_id = int(book_id) data, last_modified = self.ajax_book_to_json( book_id, get_category_urls=category_urls.lower() == 'true', device_compatible=device_compatible.lower() == 'true', device_for_template=device_for_template) except: raise cherrypy.HTTPError(404, 'No book with id: %r' % book_id) cherrypy.response.headers['Last-Modified'] = \ self.last_modified(last_modified) return data @Endpoint(set_last_modified=False) def ajax_books(self, ids=None, category_urls='true', id_is_uuid='false', device_for_template=None): ''' Return the metadata for a list of books specified as a comma separated list of ids. The metadata is returned as a dictionary mapping ids to the metadata. The format for the metadata is the same as in ajax_book(). If no book is found for a given id, it is mapped to null in the dictionary. This endpoint can be used with either GET or POST requests, variable name is ids: /ajax/books?ids=1,2,3,4,5 ''' if ids is None: raise cherrypy.HTTPError(404, 'Must specify some ids') try: if id_is_uuid == 'true': ids = set(self.db.get_id_from_uuid(x) for x in ids.split(',')) else: ids = set(int(x.strip()) for x in ids.split(',')) except: raise cherrypy.HTTPError( 404, 'ids must be a comma separated list' ' of integers') ans = {} lm = None gcu = category_urls.lower() == 'true' for book_id in ids: try: data, last_modified = self.ajax_book_to_json( book_id, get_category_urls=gcu, device_for_template=device_for_template) except: ans[book_id] = None else: ans[book_id] = data if lm is None or last_modified > lm: lm = last_modified cherrypy.response.timeout = 3600 cherrypy.response.headers['Last-Modified'] = \ self.last_modified(lm if lm is not None else self.db.last_modified()) return ans # }}} # Top level categories {{{ @Endpoint() def ajax_categories(self): ''' Return the list of top-level categories as a list of dictionaries. Each dictionary is of the form:: { 'name': Display Name, 'url':URL that gives the JSON object corresponding to all entries in this category, 'icon': URL to icon of this category, 'is_category': False for the All Books and Newest categories, True for everything else } ''' ans = {} categories = self.categories_cache() category_meta = self.db.field_metadata def getter(x): return category_meta[x]['name'] displayed_custom_fields = custom_fields_to_display(self.db) 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 if category_meta.is_ignorable_field(category) and \ category not in displayed_custom_fields: continue display_name = meta['name'] if category.startswith('@'): category = category.partition('.')[0] display_name = category[1:] url = force_unicode(category) icon = category_icon(category, meta) ans[url] = (display_name, icon) ans = [{ 'url': k, 'name': v[0], 'icon': v[1], 'is_category': True } for k, v in ans.iteritems()] ans.sort(key=lambda x: sort_key(x['name'])) for name, url, icon in [ (_('All books'), 'allbooks', 'book.png'), (_('Newest'), 'newest', 'forward.png'), ]: ans.insert(0, { 'name': name, 'url': url, 'icon': icon, 'is_category': False }) for c in ans: c['url'] = category_url(self.opts.url_prefix, c['url']) c['icon'] = icon_url(self.opts.url_prefix, c['icon']) return ans # }}} # Items in the specified category {{{ @Endpoint() def ajax_category(self, name, sort='title', num=100, offset=0, sort_order='asc'): ''' Return a dictionary describing the category specified by name. The dictionary looks like:: { 'category_name': Category display name, 'base_url': Base URL for this category, 'total_num': Total numberof items in this category, 'offset': The offset for the items returned in this result, 'num': The number of items returned in this result, 'sort': How the returned items are sorted, 'sort_order': asc or desc 'subcategories': List of sub categories of this category. 'items': List of items in this category, } Each subcategory is a dictionary of the same form as those returned by ajax_categories(). Each item is a dictionary of the form:: { 'name': Display name, 'average_rating': Average rating for books in this item, 'count': Number of books in this item, 'url': URL to get list of books in this item, 'has_children': If True this item contains sub categories, look for an entry corresponding to this item in subcategories in the main dictionary, } :param sort: How to sort the returned items. Choices are: name, rating, popularity :param sort_order: asc or desc To learn how to create subcategories see http://manual.calibre-ebook.com/sub_groups.html ''' try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r" % num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset) base_url = absurl(self.opts.url_prefix, '/ajax/category/' + name) if sort not in ('rating', 'name', 'popularity'): sort = 'name' if sort_order not in ('asc', 'desc'): sort_order = 'asc' try: dname = decode_name(name) except: raise cherrypy.HTTPError( 404, 'Invalid encoding of category name' ' %r' % name) if dname in ('newest', 'allbooks'): if dname == 'newest': sort, sort_order = 'timestamp', 'desc' raise cherrypy.InternalRedirect( '/ajax/books_in/%s/%s?sort=%s&sort_order=%s' % (encode_name(dname), encode_name('0'), sort, sort_order)) fm = self.db.field_metadata categories = self.categories_cache() hierarchical_categories = self.db.prefs['categories_using_hierarchy'] subcategory = dname toplevel = subcategory.partition('.')[0] if toplevel == subcategory: subcategory = None if toplevel not in categories or toplevel not in fm: raise cherrypy.HTTPError(404, 'Category %r not found' % toplevel) # Find items and sub categories subcategories = [] meta = fm[toplevel] item_names = {} children = set() if meta['kind'] == 'user': fullname = ((toplevel + '.' + subcategory) if subcategory is not None else toplevel) try: # User categories cannot be applied to books, so this is the # complete set of items, no need to consider sub categories items = categories[fullname] except: raise cherrypy.HTTPError( 404, 'User category %r not found' % fullname) parts = fullname.split('.') for candidate in categories: cparts = candidate.split('.') if len(cparts) == len(parts) + 1 and cparts[:-1] == parts: subcategories.append({ 'name': cparts[-1], 'url': candidate, 'icon': category_icon(toplevel, meta) }) category_name = toplevel[1:].split('.') # When browsing by user categories we ignore hierarchical normal # columns, so children can be empty elif toplevel in hierarchical_categories: items = [] category_names = [ x.original_name.split('.') for x in categories[toplevel] if '.' in x.original_name ] if subcategory is None: children = set(x[0] for x in category_names) category_name = [meta['name']] items = [ x for x in categories[toplevel] if '.' not in x.original_name ] else: subcategory_parts = subcategory.split('.')[1:] category_name = [meta['name']] + subcategory_parts lsp = len(subcategory_parts) children = set( '.'.join(x) for x in category_names if len(x) == lsp + 1 and x[:lsp] == subcategory_parts) items = [ x for x in categories[toplevel] if x.original_name in children ] item_names = { x: x.original_name.rpartition('.')[-1] for x in items } # Only mark the subcategories that have children themselves as # subcategories children = set( '.'.join(x[:lsp + 1]) for x in category_names if len(x) > lsp + 1 and x[:lsp] == subcategory_parts) subcategories = [{ 'name': x.rpartition('.')[-1], 'url': toplevel + '.' + x, 'icon': category_icon(toplevel, meta) } for x in children] else: items = categories[toplevel] category_name = meta['name'] for x in subcategories: x['url'] = category_url(self.opts.url_prefix, x['url']) x['icon'] = icon_url(self.opts.url_prefix, x['icon']) x['is_category'] = True sort_keygen = { 'name': lambda x: sort_key(x.sort if x.sort else x.original_name), 'popularity': lambda x: x.count, 'rating': lambda x: x.avg_rating } items.sort(key=sort_keygen[sort], reverse=sort_order == 'desc') total_num = len(items) items = items[offset:offset + num] items = [{ 'name': item_names.get(x, x.original_name), 'average_rating': x.avg_rating, 'count': x.count, 'url': books_in_url(self.opts.url_prefix, x.category if x.category else toplevel, x.original_name if x.id is None else unicode(x.id)), 'has_children': x.original_name in children, } for x in items] return { 'category_name': category_name, 'base_url': base_url, 'total_num': total_num, 'offset': offset, 'num': len(items), 'sort': sort, 'sort_order': sort_order, 'subcategories': subcategories, 'items': items, } # }}} # Books in the specified category {{{ @Endpoint() def ajax_books_in(self, category, item, sort='title', num=25, offset=0, sort_order='asc', get_additional_fields=''): ''' Return the books (as list of ids) present in the specified category. ''' try: dname, ditem = map(decode_name, (category, item)) except: raise cherrypy.HTTPError(404, 'Invalid encoded param: %r' % category) try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r" % num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset) if sort_order not in ('asc', 'desc'): sort_order = 'asc' sfield = self.db.data.sanitize_sort_field_name(sort) if sfield not in self.db.field_metadata.sortable_field_keys(): raise cherrypy.HTTPError(404, '%s is not a valid sort field' % sort) if dname in ('allbooks', 'newest'): ids = self.search_cache('') elif dname == 'search': try: ids = self.search_cache('search:"%s"' % ditem) except: raise cherrypy.HTTPError(404, 'Search: %r not understood' % ditem) else: try: cid = int(ditem) except: raise cherrypy.HTTPError( 404, 'Category id %r not an integer' % ditem) if dname == 'news': dname = 'tags' ids = self.db.get_books_for_category(dname, cid) all_ids = set(self.search_cache('')) # Implement restriction ids = ids.intersection(all_ids) ids = list(ids) self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True, only_ids=ids) total_num = len(ids) ids = ids[offset:offset + num] result = { 'total_num': total_num, 'sort_order': sort_order, 'offset': offset, 'num': len(ids), 'sort': sort, 'base_url': absurl(self.opts.url_prefix, '/ajax/books_in/%s/%s' % (category, item)), 'book_ids': ids } if get_additional_fields: additional_fields = {} for field in get_additional_fields.split(','): field = field.strip() if field: flist = additional_fields[field] = [] for id_ in ids: flist.append( self.db.new_api.field_for(field, id_, default_value=None)) if additional_fields: result['additional_fields'] = additional_fields return result # }}} # Search {{{ @Endpoint() def ajax_search(self, query='', sort='title', offset=0, num=25, sort_order='asc'): ''' Return the books (as list of ids) matching the specified search query. ''' try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r" % num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset) sfield = self.db.data.sanitize_sort_field_name(sort) if sfield not in self.db.field_metadata.sortable_field_keys(): raise cherrypy.HTTPError(404, '%s is not a valid sort field' % sort) if isbytestring(query): query = query.decode('UTF-8') ids = list(self.search_for_books(query)) self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True, only_ids=ids) total_num = len(ids) ids = ids[offset:offset + num] return { 'total_num': total_num, 'sort_order': sort_order, 'offset': offset, 'num': len(ids), 'sort': sort, 'base_url': absurl(self.opts.url_prefix, '/ajax/search'), 'query': query, 'book_ids': ids }
class AjaxServer(object): def __init__(self): self.ajax_json_codec = JsonCodec() def add_routes(self, connect): base_href = '/ajax' # Metadata for books connect('ajax_book', base_href+'/book/{book_id}', self.ajax_book) connect('ajax_books', base_href+'/books', self.ajax_books) # The list of top level categories connect('ajax_categories', base_href+'/categories', self.ajax_categories) # The list of sub-categories and items in each category connect('ajax_category', base_href+'/category/{name}', self.ajax_category) # List of books in specified category connect('ajax_books_in', base_href+'/books_in/{category}/{item}', self.ajax_books_in) # Search connect('ajax_search', base_href+'/search', self.ajax_search) # Get book metadata {{{ def ajax_book_to_json(self, book_id, get_category_urls=True, device_compatible=False, device_for_template=None): mi = self.db.get_metadata(book_id, index_is_id=True) if not device_compatible: try: mi.rating = mi.rating/2. except: mi.rating = 0.0 data = self.ajax_json_codec.encode_book_metadata(mi) for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime', 'rights', 'book_producer'): data.pop(x, None) data['cover'] = absurl(self.opts.url_prefix, u'/get/cover/%d'%book_id) data['thumbnail'] = absurl(self.opts.url_prefix, u'/get/thumb/%d'%book_id) if not device_compatible: mi.format_metadata = {k.lower():dict(v) for k, v in mi.format_metadata.iteritems()} for v in mi.format_metadata.itervalues(): mtime = v.get('mtime', None) if mtime is not None: v['mtime'] = isoformat(mtime, as_utc=True) data['format_metadata'] = mi.format_metadata fmts = set(x.lower() for x in mi.format_metadata.iterkeys()) pf = prefs['output_format'].lower() other_fmts = list(fmts) try: fmt = pf if pf in fmts else other_fmts[0] except: fmt = None if fmts and fmt: other_fmts = [x for x in fmts if x != fmt] data['formats'] = sorted(fmts) if fmt: data['main_format'] = {fmt: absurl(self.opts.url_prefix, u'/get/%s/%d'%(fmt, book_id))} else: data['main_format'] = None data['other_formats'] = {fmt: absurl(self.opts.url_prefix, u'/get/%s/%d'%(fmt, book_id)) for fmt in other_fmts} if get_category_urls: category_urls = data['category_urls'] = {} ccache = self.categories_cache() for key in mi.all_field_keys(): fm = mi.metadata_for_field(key) if (fm and fm['is_category'] and not fm['is_csp'] and key != 'formats' and fm['datatype'] not in ['rating']): categories = mi.get(key) if isinstance(categories, basestring): categories = [categories] if categories is None: categories = [] dbtags = {} for category in categories: for tag in ccache.get(key, []): if tag.original_name == category: dbtags[category] = books_in_url(self.opts.url_prefix, tag.category if tag.category else key, tag.original_name if tag.id is None else unicode(tag.id)) break category_urls[key] = dbtags else: series = data.get('series', None) if series: tsorder = tweaks['save_template_title_series_sorting'] series = title_sort(series, order=tsorder) else: series = '' data['_series_sort_'] = series if device_for_template: import posixpath from calibre.devices.utils import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize from calibre.customize.ui import device_plugins for device_class in device_plugins(): if device_class.__class__.__name__ == device_for_template: template = device_class.save_template() data['_filename_'] = create_upload_path(mi, book_id, template, sanitize, path_type=posixpath) break return data, mi.last_modified @Endpoint(set_last_modified=False) def ajax_book(self, book_id, category_urls='true', id_is_uuid='false', device_compatible='false', device_for_template=None): ''' Return the metadata of the book as a JSON dictionary. If category_urls == 'true' the returned dictionary also contains a mapping of category names to URLs that return the list of books in the given category. ''' cherrypy.response.timeout = 3600 try: if id_is_uuid == 'true': book_id = self.db.get_id_from_uuid(book_id) else: book_id = int(book_id) data, last_modified = self.ajax_book_to_json(book_id, get_category_urls=category_urls.lower()=='true', device_compatible=device_compatible.lower()=='true', device_for_template=device_for_template) except: raise cherrypy.HTTPError(404, 'No book with id: %r'%book_id) cherrypy.response.headers['Last-Modified'] = \ self.last_modified(last_modified) return data @Endpoint(set_last_modified=False) def ajax_books(self, ids=None, category_urls='true', id_is_uuid='false', device_for_template=None): ''' Return the metadata for a list of books specified as a comma separated list of ids. The metadata is returned as a dictionary mapping ids to the metadata. The format for the metadata is the same as in ajax_book(). If no book is found for a given id, it is mapped to null in the dictionary. This endpoint can be used with either GET or POST requests, variable name is ids: /ajax/books?ids=1,2,3,4,5 ''' if ids is None: raise cherrypy.HTTPError(404, 'Must specify some ids') try: if id_is_uuid == 'true': ids = set(self.db.get_id_from_uuid(x) for x in ids.split(',')) else: ids = set(int(x.strip()) for x in ids.split(',')) except: raise cherrypy.HTTPError(404, 'ids must be a comma separated list' ' of integers') ans = {} lm = None gcu = category_urls.lower()=='true' for book_id in ids: try: data, last_modified = self.ajax_book_to_json(book_id, get_category_urls=gcu, device_for_template=device_for_template) except: ans[book_id] = None else: ans[book_id] = data if lm is None or last_modified > lm: lm = last_modified cherrypy.response.timeout = 3600 cherrypy.response.headers['Last-Modified'] = \ self.last_modified(lm if lm is not None else self.db.last_modified()) return ans # }}} # Top level categories {{{ @Endpoint() def ajax_categories(self): ''' Return the list of top-level categories as a list of dictionaries. Each dictionary is of the form:: { 'name': Display Name, 'url':URL that gives the JSON object corresponding to all entries in this category, 'icon': URL to icon of this category, 'is_category': False for the All Books and Newest categories, True for everything else } ''' ans = {} categories = self.categories_cache() category_meta = self.db.field_metadata def getter(x): return category_meta[x]['name'] displayed_custom_fields = custom_fields_to_display(self.db) 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 if category_meta.is_ignorable_field(category) and \ category not in displayed_custom_fields: continue display_name = meta['name'] if category.startswith('@'): category = category.partition('.')[0] display_name = category[1:] url = force_unicode(category) icon = category_icon(category, meta) ans[url] = (display_name, icon) ans = [{'url':k, 'name':v[0], 'icon':v[1], 'is_category':True} for k, v in ans.iteritems()] ans.sort(key=lambda x: sort_key(x['name'])) for name, url, icon in [ (_('All books'), 'allbooks', 'book.png'), (_('Newest'), 'newest', 'forward.png'), ]: ans.insert(0, {'name':name, 'url':url, 'icon':icon, 'is_category':False}) for c in ans: c['url'] = category_url(self.opts.url_prefix, c['url']) c['icon'] = icon_url(self.opts.url_prefix, c['icon']) return ans # }}} # Items in the specified category {{{ @Endpoint() def ajax_category(self, name, sort='title', num=100, offset=0, sort_order='asc'): ''' Return a dictionary describing the category specified by name. The dictionary looks like:: { 'category_name': Category display name, 'base_url': Base URL for this category, 'total_num': Total numberof items in this category, 'offset': The offset for the items returned in this result, 'num': The number of items returned in this result, 'sort': How the returned items are sorted, 'sort_order': asc or desc 'subcategories': List of sub categories of this category. 'items': List of items in this category, } Each subcategory is a dictionary of the same form as those returned by ajax_categories(). Each item is a dictionary of the form:: { 'name': Display name, 'average_rating': Average rating for books in this item, 'count': Number of books in this item, 'url': URL to get list of books in this item, 'has_children': If True this item contains sub categories, look for an entry corresponding to this item in subcategories in the main dictionary, } :param sort: How to sort the returned items. Choices are: name, rating, popularity :param sort_order: asc or desc To learn how to create subcategories see http://manual.calibre-ebook.com/sub_groups.html ''' try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r"%num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset) base_url = absurl(self.opts.url_prefix, '/ajax/category/'+name) if sort not in ('rating', 'name', 'popularity'): sort = 'name' if sort_order not in ('asc', 'desc'): sort_order = 'asc' try: dname = decode_name(name) except: raise cherrypy.HTTPError(404, 'Invalid encoding of category name' ' %r'%name) if dname in ('newest', 'allbooks'): if dname == 'newest': sort, sort_order = 'timestamp', 'desc' raise cherrypy.InternalRedirect( '/ajax/books_in/%s/%s?sort=%s&sort_order=%s'%( encode_name(dname), encode_name('0'), sort, sort_order)) fm = self.db.field_metadata categories = self.categories_cache() hierarchical_categories = self.db.prefs['categories_using_hierarchy'] subcategory = dname toplevel = subcategory.partition('.')[0] if toplevel == subcategory: subcategory = None if toplevel not in categories or toplevel not in fm: raise cherrypy.HTTPError(404, 'Category %r not found'%toplevel) # Find items and sub categories subcategories = [] meta = fm[toplevel] item_names = {} children = set() if meta['kind'] == 'user': fullname = ((toplevel + '.' + subcategory) if subcategory is not None else toplevel) try: # User categories cannot be applied to books, so this is the # complete set of items, no need to consider sub categories items = categories[fullname] except: raise cherrypy.HTTPError(404, 'User category %r not found'%fullname) parts = fullname.split('.') for candidate in categories: cparts = candidate.split('.') if len(cparts) == len(parts)+1 and cparts[:-1] == parts: subcategories.append({'name':cparts[-1], 'url':candidate, 'icon':category_icon(toplevel, meta)}) category_name = toplevel[1:].split('.') # When browsing by user categories we ignore hierarchical normal # columns, so children can be empty elif toplevel in hierarchical_categories: items = [] category_names = [x.original_name.split('.') for x in categories[toplevel] if '.' in x.original_name] if subcategory is None: children = set(x[0] for x in category_names) category_name = [meta['name']] items = [x for x in categories[toplevel] if '.' not in x.original_name] else: subcategory_parts = subcategory.split('.')[1:] category_name = [meta['name']] + subcategory_parts lsp = len(subcategory_parts) children = set('.'.join(x) for x in category_names if len(x) == lsp+1 and x[:lsp] == subcategory_parts) items = [x for x in categories[toplevel] if x.original_name in children] item_names = {x:x.original_name.rpartition('.')[-1] for x in items} # Only mark the subcategories that have children themselves as # subcategories children = set('.'.join(x[:lsp+1]) for x in category_names if len(x) > lsp+1 and x[:lsp] == subcategory_parts) subcategories = [{'name':x.rpartition('.')[-1], 'url':toplevel+'.'+x, 'icon':category_icon(toplevel, meta)} for x in children] else: items = categories[toplevel] category_name = meta['name'] for x in subcategories: x['url'] = category_url(self.opts.url_prefix, x['url']) x['icon'] = icon_url(self.opts.url_prefix, x['icon']) x['is_category'] = True sort_keygen = { 'name': lambda x: sort_key(x.sort if x.sort else x.original_name), 'popularity': lambda x: x.count, 'rating': lambda x: x.avg_rating } items.sort(key=sort_keygen[sort], reverse=sort_order == 'desc') total_num = len(items) items = items[offset:offset+num] items = [{ 'name':item_names.get(x, x.original_name), 'average_rating': x.avg_rating, 'count': x.count, 'url': books_in_url(self.opts.url_prefix, x.category if x.category else toplevel, x.original_name if x.id is None else unicode(x.id)), 'has_children': x.original_name in children, } for x in items] return { 'category_name': category_name, 'base_url': base_url, 'total_num': total_num, 'offset':offset, 'num':len(items), 'sort':sort, 'sort_order':sort_order, 'subcategories':subcategories, 'items':items, } # }}} # Books in the specified category {{{ @Endpoint() def ajax_books_in(self, category, item, sort='title', num=25, offset=0, sort_order='asc', get_additional_fields=''): ''' Return the books (as list of ids) present in the specified category. ''' try: dname, ditem = map(decode_name, (category, item)) except: raise cherrypy.HTTPError(404, 'Invalid encoded param: %r'%category) try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r"%num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset) if sort_order not in ('asc', 'desc'): sort_order = 'asc' sfield = self.db.data.sanitize_sort_field_name(sort) if sfield not in self.db.field_metadata.sortable_field_keys(): raise cherrypy.HTTPError(404, '%s is not a valid sort field'%sort) if dname in ('allbooks', 'newest'): ids = self.search_cache('') elif dname == 'search': try: ids = self.search_cache('search:"%s"'%ditem) except: raise cherrypy.HTTPError(404, 'Search: %r not understood'%ditem) else: try: cid = int(ditem) except: raise cherrypy.HTTPError(404, 'Category id %r not an integer'%ditem) if dname == 'news': dname = 'tags' ids = self.db.get_books_for_category(dname, cid) all_ids = set(self.search_cache('')) # Implement restriction ids = ids.intersection(all_ids) ids = list(ids) self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True, only_ids=ids) total_num = len(ids) ids = ids[offset:offset+num] result = { 'total_num': total_num, 'sort_order':sort_order, 'offset':offset, 'num':len(ids), 'sort':sort, 'base_url':absurl(self.opts.url_prefix, '/ajax/books_in/%s/%s'%(category, item)), 'book_ids':ids } if get_additional_fields: additional_fields = {} for field in get_additional_fields.split(','): field = field.strip() if field: flist = additional_fields[field] = [] for id_ in ids: flist.append(self.db.new_api.field_for(field, id_, default_value=None)) if additional_fields: result['additional_fields'] = additional_fields return result # }}} # Search {{{ @Endpoint() def ajax_search(self, query='', sort='title', offset=0, num=25, sort_order='asc'): ''' Return the books (as list of ids) matching the specified search query. ''' try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r"%num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset) sfield = self.db.data.sanitize_sort_field_name(sort) if sfield not in self.db.field_metadata.sortable_field_keys(): raise cherrypy.HTTPError(404, '%s is not a valid sort field'%sort) if isbytestring(query): query = query.decode('UTF-8') ids = list(self.search_for_books(query)) self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True, only_ids=ids) total_num = len(ids) ids = ids[offset:offset+num] return { 'total_num': total_num, 'sort_order':sort_order, 'offset':offset, 'num':len(ids), 'sort':sort, 'base_url':absurl(self.opts.url_prefix, '/ajax/search'), 'query': query, 'book_ids':ids }
def book_to_json(ctx, rd, db, book_id, get_category_urls=True, device_compatible=False, device_for_template=None): mi = db.get_metadata(book_id, get_cover=False) codec = JsonCodec(db.field_metadata) if not device_compatible: try: mi.rating = mi.rating/2. except Exception: mi.rating = 0.0 data = codec.encode_book_metadata(mi) for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime', 'rights', 'book_producer'): data.pop(x, None) get = partial(ctx.url_for, get_content, book_id=book_id, library_id=db.server_library_id) data['cover'] = get(what='cover') data['thumbnail'] = get(what='thumb') if not device_compatible: mi.format_metadata = {k.lower():dict(v) for k, v in mi.format_metadata.iteritems()} for v in mi.format_metadata.itervalues(): mtime = v.get('mtime', None) if mtime is not None: v['mtime'] = isoformat(mtime, as_utc=True) data['format_metadata'] = mi.format_metadata fmts = set(x.lower() for x in mi.format_metadata.iterkeys()) pf = prefs['output_format'].lower() other_fmts = list(fmts) try: fmt = pf if pf in fmts else other_fmts[0] except: fmt = None if fmts and fmt: other_fmts = [x for x in fmts if x != fmt] data['formats'] = sorted(fmts) if fmt: data['main_format'] = {fmt:get(what=fmt)} else: data['main_format'] = None data['other_formats'] = {fmt:get(what=fmt) for fmt in other_fmts} if get_category_urls: category_urls = data['category_urls'] = {} all_cats = ctx.get_categories(rd, db) for key in mi.all_field_keys(): fm = mi.metadata_for_field(key) if (fm and fm['is_category'] and not fm['is_csp'] and key != 'formats' and fm['datatype'] != 'rating'): categories = mi.get(key) or [] if isinstance(categories, basestring): categories = [categories] category_urls[key] = dbtags = {} for category in categories: for tag in all_cats.get(key, ()): if tag.original_name == category: dbtags[category] = ctx.url_for( books_in, encoded_category=encode_name(tag.category if tag.category else key), encoded_item=encode_name(tag.original_name if tag.id is None else unicode(tag.id)), library_id=db.server_library_id ) break else: series = data.get('series', None) or '' if series: tsorder = tweaks['save_template_title_series_sorting'] series = title_sort(series, order=tsorder) data['_series_sort_'] = series if device_for_template: import posixpath from calibre.devices.utils import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize from calibre.customize.ui import device_plugins for device_class in device_plugins(): if device_class.__class__.__name__ == device_for_template: template = device_class.save_template() data['_filename_'] = create_upload_path(mi, book_id, template, sanitize, path_type=posixpath) break return data, mi.last_modified
class BrowseServer(object): def __init__(self): self.json_codec = JsonCodec() def add_routes(self, connect): base_href = '/browse' connect('browse', base_href, self.browse_catalog) connect('browse_catalog', base_href+'/category/{category}', self.browse_catalog) connect('browse_category_group', base_href+'/category_group/{category}/{group}', self.browse_category_group) connect('browse_matches', base_href+'/matches/{category}/{cid}', self.browse_matches) connect('browse_booklist_page', base_href+'/booklist_page', self.browse_booklist_page) connect('browse_search', base_href+'/search', self.browse_search) connect('browse_details', base_href+'/details/{id}', self.browse_details) connect('browse_book', base_href+'/book/{id}', self.browse_book) connect('browse_random', base_href+'/random', self.browse_random) connect('browse_category_icon', base_href+'/icon/{name}', self.browse_icon) self.icon_map = JSONConfig('gui').get('tags_browser_category_icons', {}) self.nav = self.browse_nav() # Templates {{{ def browse_template(self, sort, category=True, initial_search=''): if not hasattr(self, '__browse_template__') or \ self.opts.develop: self.__browse_template__ = \ P('content_server/browse/browse.html', data=True).decode('utf-8') ans = self.__browse_template__ scn = 'calibre_browse_server_sort_' if category: sort_opts = [('rating', _('Average rating')), ('name', _('Name')), ('popularity', _('Popularity'))] scn += 'category' else: scn += 'list' fm = self.db.field_metadata sort_opts, added = [], set([]) displayed_custom_fields = custom_fields_to_display(self.db) for x in fm.sortable_field_keys(): if x in ('ondevice', 'formats', 'sort'): continue if fm.is_ignorable_field(x) and x not in displayed_custom_fields: continue if x == 'comments' or fm[x]['datatype'] == 'comments': continue n = fm[x]['name'] if n not in added: added.add(n) sort_opts.append((x, n)) ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':')) ans = ans.replace('{sort_cookie_name}', scn) ans = ans.replace('{prefix}', self.opts.url_prefix) ans = ans.replace('{library}', _('library')) ans = ans.replace('{home}', _('home')) ans = ans.replace('{Search}', _('Search')) ans = ans.replace('{nav}', self.nav) opts = ['<option %svalue="%s">%s</option>' % ( 'selected="selected" ' if k==sort else '', xml(k), xml(nl), ) for k, nl in sorted(sort_opts, key=lambda x: sort_key(operator.itemgetter(1)(x))) if k and nl] ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts)) lp = self.db.library_path if isbytestring(lp): lp = force_unicode(lp, filesystem_encoding) ans = ans.replace('{library_name}', xml(os.path.basename(lp))) ans = ans.replace('{library_path}', xml(lp, True)) ans = ans.replace('{initial_search}', xml(initial_search, attribute=True)) return ans @property def browse_summary_template(self): if not hasattr(self, '__browse_summary_template__') or \ self.opts.develop: self.__browse_summary_template__ = \ P('content_server/browse/summary.html', data=True).decode('utf-8') return self.__browse_summary_template__.replace('{prefix}', self.opts.url_prefix) @property def browse_details_template(self): if not hasattr(self, '__browse_details_template__') or \ self.opts.develop: self.__browse_details_template__ = \ P('content_server/browse/details.html', data=True).decode('utf-8') return self.__browse_details_template__.replace('{prefix}', self.opts.url_prefix) # }}} # Catalogs {{{ def browse_icon(self, name='blank.png'): cherrypy.response.headers['Content-Type'] = 'image/png' cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) if not hasattr(self, '__browse_icon_cache__'): self.__browse_icon_cache__ = {} if name not in self.__browse_icon_cache__: if name.startswith('_'): name = sanitize_file_name2(name[1:]) try: with open(os.path.join(config_dir, 'tb_icons', name), 'rb') as f: data = f.read() except: raise cherrypy.HTTPError(404, 'no icon named: %r'%name) else: try: data = I(name, data=True) except: raise cherrypy.HTTPError(404, 'no icon named: %r'%name) img = Image() img.load(data) width, height = img.size scaled, width, height = fit_image(width, height, 48, 48) if scaled: img.size = (width, height) self.__browse_icon_cache__[name] = img.export('png') return self.__browse_icon_cache__[name] def browse_nav(self): categories = self.categories_cache() category_meta = self.db.field_metadata cats = [ (_('Newest'), 'newest', 'whatshot'), (_('All books'), 'allbooks', 'library_books'), (_('Random book'), 'randombook', 'shuffle'), ] virt_libs = self.db.prefs.get('virtual_libraries', {}) if virt_libs: cats.append((_('Virtual Libs.'), 'virt_libs', 'graphic_eq')) def getter(x): try: return category_meta[x]['name'].lower() except KeyError: return x displayed_custom_fields = custom_fields_to_display(self.db) uc_displayed = set() 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 if self.db.field_metadata.is_ignorable_field(category) and \ category not in displayed_custom_fields: continue # get the icon files main_cat = (category.partition('.')[0]) if hasattr(category, 'partition') else category if main_cat in self.icon_map: icon = self.icon_map[main_cat] elif category in category_icon_map: icon = category_icon_map[category] elif meta['is_custom']: icon = category_icon_map['custom:'] elif meta['kind'] == 'user': icon = category_icon_map['user:'******'check_box_outline_blank' if meta['kind'] == 'user': dot = category.find('.') if dot > 0: cat = category[:dot] if cat not in uc_displayed: cats.append((meta['name'][:dot-1], cat, icon)) uc_displayed.add(cat) else: cats.append((meta['name'], category, icon)) uc_displayed.add(category) else: cats.append((meta['name'], category, icon)) cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">' u'<i class="material-icons">{icon}</i>' u'<span>{0}</span></a>' u'</li>') .format(xml(name, True), xml(quote(cat)), xml(_('Browse books by')), self.opts.url_prefix, icon=icon) for name, cat, icon in cats] return '\n'.join(cats) def books_to_json(self, *books): result = [] for book in books: book_json = self.json_codec.encode_book_metadata(book) book_json['formats'] = [fmt.lower() for fmt in book.formats] book_json['format_metadata'] = {k.lower(): dict(v) for k, v in book.format_metadata.iteritems()} for v in book_json['format_metadata'].itervalues(): v.pop('mtime', None) result.append(book_json) return json.dumps(result, skipkeys=True) def browse_json_books(self, ids, prefix, suffix=''): books = [self.db.get_metadata(id, True) for id in ids] books_json = self.books_to_json(*books) return "<script>window.calibre_books = {}</script>".format(books_json) def browse_toplevel(self): main = u'<div class="toplevel"></div>' return self.browse_template('name').format(title='', main=main) def browse_sort_categories(self, items, sort): if sort not in ('rating', 'name', 'popularity'): sort = 'name' items.sort(key=lambda x: sort_key(getattr(x, 'sort', x.name))) if sort == 'popularity': items.sort(key=operator.attrgetter('count'), reverse=True) elif sort == 'rating': items.sort(key=operator.attrgetter('avg_rating'), reverse=True) return sort def browse_category(self, category, sort): categories = self.categories_cache() categories['virt_libs'] = {} if category not in categories: raise cherrypy.HTTPError(404, 'category not found') category_meta = self.db.field_metadata category_name = _('Virtual Libraries') if category == 'virt_libs' else category_meta[category]['name'] datatype = 'text' if category == 'virt_libs' else category_meta[category]['datatype'] # See if we have any sub-categories to display. As we find them, add # them to the displayed set to avoid showing the same item twice uc_displayed = set() cats = [] for ucat in sorted(categories.keys(), key=sort_key): if len(categories[ucat]) == 0: continue if category == 'formats': continue meta = category_meta.get(ucat, None) if meta is None: continue if meta['kind'] != 'user': continue cat_len = len(category) if not (len(ucat) > cat_len and ucat.startswith(category+'.')): continue if ucat in self.icon_map: icon = '_'+quote(self.icon_map[ucat]) else: icon = category_icon_map['user:'******'.') if dot > 0: # More subcats cat = cat[:dot] if cat not in uc_displayed: cats.append((cat, ucat[:cat_len+dot], icon)) uc_displayed.add(cat) else: # This is the end of the chain cats.append((cat, ucat, icon)) uc_displayed.add(cat) cats = u'\n\n'.join( [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>' u'<img src="{3}{src}" alt="{0}" />' u'<span class="label">{0}</span>' u'</li>') .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')), self.opts.url_prefix, src='/browse/icon/'+z) for x, y, z in cats]) if cats: cats = (u'\n<div class="toplevel">\n' '{0}</div>').format(cats) # Now do the category items vls = self.db.prefs.get('virtual_libraries', {}) categories['virt_libs'] = sorted([Tag(k) for k, v in vls.iteritems()], key=lambda x:sort_key(x.name)) items = categories[category] print(items) sort = self.browse_sort_categories(items, sort) if not cats and len(items) == 1: # Only one item in category, go directly to book list html = get_category_items(category, items, datatype, self.opts.url_prefix) href = re.search(r'<a href="([^"]+)"', html) if href is not None: # cherrypy does not auto unquote params when using # InternalRedirect raise cherrypy.InternalRedirect(unquote(href.group(1))) if len(items) <= self.opts.max_opds_ungrouped_items: items = get_category_items(category, items, datatype, self.opts.url_prefix) else: getter = lambda x: unicode(getattr(x, 'sort', None) or x.name) starts = set([]) for x in items: val = getter(x) if not val: val = u'A' starts.add(val[0].upper()) category_groups = OrderedDict() for x in sorted(starts): category_groups[x] = len([y for y in items if getter(y).upper().startswith(x)]) items = [(u'<h3 title="{0}"><a class="load_href" title="{0}"' u' href="{4}{3}"><strong>{0}</strong> [{2}]</a></h3><div>' u'<div class="loaded" style="display:none"></div>' u'<div class="loading"><img alt="{1}" src="{4}/static/loading.gif" /><em>{1}</em></div>' u'</div>').format( xml(s, True), xml(_('Loading, please wait'))+'…', unicode(c), xml(u'/browse/category_group/%s/%s'%( hexlify(category.encode('utf-8')), hexlify(s.encode('utf-8'))), True), self.opts.url_prefix) for s, c in category_groups.items()] items = '\n\n'.join(items) items = u'<div id="groups">\n{0}</div>'.format(items) main = u''' <div class="category"> <h3>{0}</h3> <a class="navlink" href="{3}/browse" title="{2}">{2} ↑</a> {1} </div> '''.format( xml(_('Browsing by')+': ' + category_name), cats + items, xml(_('Up'), True), self.opts.url_prefix) return self.browse_template(sort).format(title=category_name, main=main) @Endpoint(mimetype='application/json; charset=utf-8') def browse_category_group(self, category=None, group=None, sort=None): if sort == 'null': sort = None if sort not in ('rating', 'name', 'popularity'): sort = 'name' try: category = unhexlify(category) if isbytestring(category): category = category.decode('utf-8') except: raise cherrypy.HTTPError(404, 'invalid category') categories = self.categories_cache() if category not in categories: raise cherrypy.HTTPError(404, 'category not found') category_meta = self.db.field_metadata try: datatype = category_meta[category]['datatype'] except KeyError: datatype = 'text' try: group = unhexlify(group) if isbytestring(group): group = group.decode('utf-8') except: raise cherrypy.HTTPError(404, 'invalid group') items = categories[category] entries = [] getter = lambda x: unicode(getattr(x, 'sort', None) or x.name) for x in items: val = getter(x) if not val: val = u'A' if val.upper().startswith(group): entries.append(x) sort = self.browse_sort_categories(entries, sort) entries = get_category_items(category, entries, datatype, self.opts.url_prefix) return json.dumps(entries, ensure_ascii=True) @Endpoint() def browse_catalog(self, category=None, category_sort=None): 'Entry point for top-level, categories and sub-categories' prefix = '' if self.is_wsgi else self.opts.url_prefix if category is None: ans = self.browse_toplevel() # The following are fake categories used for the top-level view elif category == 'newest': raise cherrypy.InternalRedirect(prefix + '/browse/matches/newest/dummy') elif category == 'allbooks': raise cherrypy.InternalRedirect(prefix + '/browse/matches/allbooks/dummy') elif category == 'randombook': raise cherrypy.InternalRedirect(prefix + '/browse/random') else: ans = self.browse_category(category, category_sort) return ans # }}} # Book Lists {{{ def browse_sort_book_list(self, items, sort): fm = self.db.field_metadata keys = frozenset(fm.sortable_field_keys()) if sort not in keys: sort = 'title' self.sort(items, 'title', True) if sort != 'title': ascending = fm[sort]['datatype'] not in ('rating', 'datetime', 'series') self.sort(items, sort, ascending) return sort @Endpoint(sort_type='list') def browse_matches(self, category=None, cid=None, list_sort=None): if list_sort: list_sort = unquote(list_sort) if not cid: raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid) categories = self.categories_cache() if category not in categories and \ category not in ('newest', 'allbooks', 'virt_libs'): raise cherrypy.HTTPError(404, 'category not found') fm = self.db.field_metadata try: category_name = fm[category]['name'] dt = fm[category]['datatype'] except: if category not in ('newest', 'allbooks', 'virt_libs'): raise category_name = { 'newest' : _('Newest'), 'allbooks' : _('All books'), 'virt_libs': _('Virtual Libraries'), }[category] dt = None hide_sort = 'true' if dt == 'series' else 'false' if category == 'search': which = unhexlify(cid).decode('utf-8') try: ids = self.search_cache('search:"%s"'%which) except: raise cherrypy.HTTPError(404, 'Search: %r not understood'%which) else: all_ids = self.search_cache('') if category == 'newest': ids = all_ids hide_sort = 'true' elif category == 'allbooks': ids = all_ids elif category == 'virt_libs': which = unhexlify(cid).decode('utf-8') vls = self.db.prefs.get('virtual_libraries', {}) ids = self.search_cache(vls[which]) category_name = _('virtual library: ') + xml(which) if not ids: msg = _('The virtual library <b>%s</b> has no books.') % prepare_string_for_xml(which) if self.search_restriction: msg += ' ' + _( 'This is probably because you have applied a virtual library' ' to the content server in Preferences->Sharing over the net.' ' This virtual library is applied globally and combined with' ' the current virtual library.') return self.browse_template('name').format(title='', main='<p>%s</p>'%msg) else: if fm.get(category, {'datatype':None})['datatype'] == 'composite': cid = cid.decode('utf-8') q = category if q == 'news': q = 'tags' ids = self.db.get_books_for_category(q, cid) ids = [x for x in ids if x in all_ids] items = [self.db.data.tablerow_for_id(x) for x in ids] if category == 'newest': list_sort = 'timestamp' if dt == 'series': list_sort = category sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] html = self.browse_json_books(ids, self.opts.url_prefix, suffix=_('in') + ' ' + category_name) return self.browse_template(sort, category=False).format( title=_('Books in') + " " +category_name, main=html) def browse_get_book_args(self, mi, id_, add_category_links=False): fmts = self.db.formats(id_, index_is_id=True) if not fmts: fmts = '' fmts = [x.lower() for x in fmts.split(',') if x] pf = prefs['output_format'].lower() try: fmt = pf if pf in fmts else fmts[0] except: fmt = None args = {'id':id_, 'mi':mi, } ccache = self.categories_cache() if add_category_links else {} ftitle = fauthors = '' for key in mi.all_field_keys(): val = mi.format_field(key)[1] if not val: val = '' if key == 'title': ftitle = xml(val, True) elif key == 'authors': fauthors = xml(val, True) if add_category_links: added_key = False fm = mi.metadata_for_field(key) if val and fm and fm['is_category'] and not fm['is_csp'] and\ key != 'formats' and fm['datatype'] not in ['rating']: categories = mi.get(key) if isinstance(categories, basestring): categories = [categories] dbtags = [] for category in categories: dbtag = None for tag in ccache[key]: if tag.name == category: dbtag = tag break dbtags.append(dbtag) if None not in dbtags: vals = [] for tag in dbtags: tval = ('<a title="Browse books by {3}: {0}"' ' href="{1}" class="details_category_link">{2}</a>') href='%s/browse/matches/%s/%s' % \ (self.opts.url_prefix, quote(tag.category), quote(str(tag.id))) vals.append(tval.format(xml(tag.name, True), xml(href, True), xml(val if len(dbtags) == 1 else tag.name), xml(key, True))) join = ' & ' if key == 'authors' or \ (fm['is_custom'] and fm['display'].get('is_names', False)) \ else ', ' args[key] = join.join(vals) added_key = True if not added_key: args[key] = xml(val, True) else: args[key] = xml(val, True) fname = quote(ascii_filename(ftitle) + ' - ' + ascii_filename(fauthors)) return args, fmt, fmts, fname @Endpoint(mimetype='application/json; charset=utf-8') def browse_booklist_page(self, ids=None, sort=None): if sort == 'null': sort = None if ids is None: ids = json.dumps('[]') try: ids = json.loads(ids) except: raise cherrypy.HTTPError(404, 'invalid ids') summs = [] for id_ in ids: try: id_ = int(id_) mi = self.db.get_metadata(id_, index_is_id=True) except: continue args, fmt, fmts, fname = self.browse_get_book_args(mi, id_) args['other_formats'] = '' args['fmt'] = fmt if fmts and fmt: other_fmts = [x for x in fmts if x.lower() != fmt.lower()] if other_fmts: ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>' .format(f, fname, id_, f.upper(), self.opts.url_prefix) for f in other_fmts] ofmts = ', '.join(ofmts) args['other_formats'] = u'<strong>%s: </strong>' % \ _('Other formats') + ofmts args['details_href'] = self.opts.url_prefix + '/browse/details/'+str(id_) if fmt: href = self.opts.url_prefix + '/get/%s/%s_%d.%s'%( fmt, fname, id_, fmt) rt = xml(_('Read %(title)s in the %(fmt)s format')% {'title':args['title'], 'fmt':fmt.upper()}, True) args['get_button'] = \ '<a href="%s" class="read" title="%s">%s</a>' % \ (xml(href, True), rt, xml(_('Get'))) args['get_url'] = xml(href, True) else: args['get_button'] = '' args['get_url'] = 'javascript:alert(\'%s\')' % xml(_( 'This book has no available formats to view'), True) args['comments'] = comments_to_html(mi.comments) args['stars'] = '' if mi.rating: args['stars'] = render_rating(mi.rating/2.0, self.opts.url_prefix, prefix=_('Rating'))[0] if args['tags']: args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \ args['tags'] if args['series']: args['series'] = args['series'] args['details'] = xml(_('Details'), True) args['details_tt'] = xml(_('Show book details'), True) args['permalink'] = xml(_('Permalink'), True) args['permalink_tt'] = xml(_('A permanent link to this book'), True) summs.append(self.browse_summary_template.format(**args)) raw = json.dumps('\n'.join(summs), ensure_ascii=True) return raw def browse_render_details(self, id_, add_random_button=False, add_title=False): try: mi = self.db.get_metadata(id_, index_is_id=True) except: return _('This book has been deleted') else: args, fmt, fmts, fname = self.browse_get_book_args(mi, id_, add_category_links=True) args['fmt'] = fmt if fmt: args['get_url'] = xml(self.opts.url_prefix + '/get/%s/%s_%d.%s'%( fmt, fname, id_, fmt), True) else: args['get_url'] = 'javascript:alert(\'%s\')' % xml(_( 'This book has no available formats to view'), True) args['formats'] = '' if fmts: ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>' .format(xfmt, fname, id_, xfmt.upper(), self.opts.url_prefix) for xfmt in fmts] ofmts = ', '.join(ofmts) args['formats'] = ofmts fields, comments = [], [] displayed_custom_fields = custom_fields_to_display(self.db) for field, m in list(mi.get_all_standard_metadata(False).items()) + \ list(mi.get_all_user_metadata(False).items()): if self.db.field_metadata.is_ignorable_field(field) and \ field not in displayed_custom_fields: continue if m['datatype'] == 'comments' or field == 'comments' or ( m['datatype'] == 'composite' and m['display'].get('contains_html', False)): val = mi.get(field, '') if val and val.strip(): comments.append((m['name'], comments_to_html(val))) continue if field in ('title', 'formats') or not args.get(field, False) \ or not m['name']: continue if field == 'identifiers': urls = urls_from_identifiers(mi.get(field, {})) links = [u'<a class="details_category_link" target="_new" href="%s" title="%s:%s">%s</a>' % (url, id_typ, id_val, name) for name, id_typ, id_val, url in urls] links = u', '.join(links) if links: fields.append((field, m['name'], u'<strong>%s: </strong>%s'%( _('Ids'), links))) continue if m['datatype'] == 'rating': r = u'<strong>%s: </strong>'%xml(m['name']) + \ render_rating(mi.get(field)/2.0, self.opts.url_prefix, prefix=m['name'])[0] else: r = u'<strong>%s: </strong>'%xml(m['name']) + \ args[field] fields.append((field, m['name'], r)) def fsort(x): num = {'authors':0, 'series':1, 'tags':2}.get(x[0], 100) return (num, sort_key(x[-1])) fields.sort(key=fsort) if add_title: fields.insert(0, ('title', 'Title', u'<strong>%s: </strong>%s' % (xml(_('Title')), xml(mi.title)))) fields = [u'<div class="field">{0}</div>'.format(f[-1]) for f in fields] fields = u'<div class="fields">%s</div>'%('\n\n'.join(fields)) comments.sort(key=lambda x: x[0].lower()) comments = [(u'<div class="field"><strong>%s: </strong>' u'<div class="comment">%s</div></div>') % (xml(c[0]), c[1]) for c in comments] comments = u'<div class="comments">%s</div>'%('\n\n'.join(comments)) random = '' if add_random_button: href = '%s/browse/random?v=%s'%( self.opts.url_prefix, time.time()) random = '<a href="%s" id="random_button" title="%s">%s</a>' % ( xml(href, True), xml(_('Choose another random book'), True), xml(_('Another random book'))) return self.browse_details_template.format( id=id_, title=xml(mi.title, True), fields=fields, get_url=args['get_url'], fmt=args['fmt'], formats=args['formats'], comments=comments, random=random) @Endpoint(mimetype='application/json; charset=utf-8') def browse_details(self, id=None): try: id_ = int(id) except: raise cherrypy.HTTPError(404, 'invalid id: %r'%id) ans = self.browse_render_details(id_) return json.dumps(ans, ensure_ascii=True) @Endpoint() def browse_random(self, *args, **kwargs): import random all_ids = list(self.search_cache('')) random.shuffle(all_ids) html = self.browse_json_books(all_ids, self.opts.url_prefix) return self.browse_template(None).format( title=_('Books in'), main=html) @Endpoint() def browse_book(self, id=None, category_sort=None): try: id_ = int(id) except: raise cherrypy.HTTPError(404, 'invalid id: %r'%id) ans = self.browse_render_details(id_, add_title=True) return self.browse_template('').format( title=prepare_string_for_xml(self.db.title(id_, index_is_id=True)), script='book();', main=ans) # }}} # Search {{{ @Endpoint() def browse_search(self, query='', list_sort=None): if isbytestring(query): query = query.decode('UTF-8') ids = self.search_for_books(query) items = [self.db.data.tablerow_for_id(x) for x in ids] sort = self.browse_sort_book_list(items, list_sort) ids = [x[0] for x in items] html = render_book_list(ids, self.opts.url_prefix, suffix=_('in search')+': '+xml(query)) return self.browse_template(sort, category=False, initial_search=query).format( title=_('Matching books'), main=html)