diff --git a/src/calibre/ebooks/chardet/__init__.py b/src/calibre/ebooks/chardet/__init__.py
index 95a44f8e56..f63805359e 100644
--- a/src/calibre/ebooks/chardet/__init__.py
+++ b/src/calibre/ebooks/chardet/__init__.py
@@ -32,8 +32,8 @@ def detect(aBuf):
ENCODING_PATS = [
re.compile(r'<\?[^<>]+encoding\s*=\s*[\'"](.*?)[\'"][^<>]*>',
re.IGNORECASE),
- re.compile(r''']*?content\s*=\s*['"][^'"]*?charset=([-a-z0-9]+)[^'"]*?['"][^<>]*>''',
- re.IGNORECASE)
+ re.compile(r''']*?content\s*=\s*['"][^'"]*?charset=([-_a-z0-9]+)[^'"]*?['"][^<>]*>''',
+ re.IGNORECASE),
]
ENTITY_PATTERN = re.compile(r'&(\S+?);')
diff --git a/src/calibre/library/server/ajax.py b/src/calibre/library/server/ajax.py
index d880ead39b..19db94b984 100644
--- a/src/calibre/library/server/ajax.py
+++ b/src/calibre/library/server/ajax.py
@@ -9,12 +9,17 @@ __docformat__ = 'restructuredtext en'
import json
from functools import wraps
+from binascii import hexlify, unhexlify
import cherrypy
from calibre.utils.date import isoformat
from calibre.utils.config import prefs
from calibre.ebooks.metadata.book.json_codec import JsonCodec
+from calibre.utils.icu import sort_key
+from calibre.library.server import custom_fields_to_display
+from calibre import force_unicode
+from calibre.library.field_metadata import category_icon_map
class Endpoint(object): # {{{
'Manage mime-type json serialization, etc.'
@@ -48,6 +53,27 @@ class Endpoint(object): # {{{
return wrapper
# }}}
+def category_icon(category, meta): # {{{
+ if 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:']
+ else:
+ icon = 'blank.png'
+ return icon
+# }}}
+
+def absurl(prefix, url):
+ return prefix + url
+
+def category_url(prefix, cid):
+ return absurl(prefix, '/ajax/category/'+cid)
+
+def icon_url(prefix, name):
+ return absurl(prefix, '/browse/icon/'+name)
+
class AjaxServer(object):
def __init__(self):
@@ -55,10 +81,23 @@ class AjaxServer(object):
def add_routes(self, connect):
base_href = '/ajax'
+
+ # Metadata for a particular book
connect('ajax_book', base_href+'/book/{book_id}', self.ajax_book)
+
+ # 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}/{name}',
+ self.ajax_books_in)
+
+
# Get book metadata {{{
@Endpoint()
def ajax_book(self, book_id):
@@ -82,11 +121,8 @@ class AjaxServer(object):
'rights', 'book_producer'):
data.pop(x, None)
- def absurl(url):
- return self.opts.url_prefix + url
-
- data['cover'] = absurl(u'/get/cover/%d'%book_id)
- data['thumbnail'] = absurl(u'/get/thumb/%d'%book_id)
+ 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)
mi.format_metadata = {k.lower():dict(v) for k, v in
mi.format_metadata.iteritems()}
for v in mi.format_metadata.itervalues():
@@ -105,15 +141,216 @@ class AjaxServer(object):
other_fmts = [x for x in fmts if x != fmt]
data['formats'] = sorted(fmts)
if fmt:
- data['main_format'] = {fmt: absurl(u'/get/%s/%d'%(fmt, book_id))}
+ 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(u'/get/%s/%d'%(fmt, book_id)) for fmt
+ data['other_formats'] = {fmt: absurl(self.opts.url_prefix, u'/get/%s/%d'%(fmt, book_id)) for fmt
in other_fmts}
return data
# }}}
+ # 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 meta['is_custom'] and category not in displayed_custom_fields:
+ continue
+ display_name = meta['name']
+ if category.startswith('@'):
+ category = category.partition('.')[0]
+ display_name = category[1:]
+ url = hexlify(force_unicode(category).encode('utf-8'))
+ 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'):
+ 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 name in ('newest', 'allbooks'):
+ if name == 'newest':
+ sort, sort_order = 'timestamp', 'desc'
+ raise cherrypy.InternalRedirect(
+ '/ajax/books_in/%s/dummy?num=%s&offset=%s&sort=%s&sort_order=%s'%(
+ name, num, offset, sort, sort_order))
+
+ if sort not in ('rating', 'name', 'popularity'):
+ sort = 'name'
+
+ if sort_order not in ('asc', 'desc'):
+ sort_order = 'asc'
+
+ fm = self.db.field_metadata
+ categories = self.categories_cache()
+ hierarchical_categories = self.db.prefs['categories_using_hierarchy']
+
+ if name in categories:
+ toplevel = name
+ subcategory = None
+ else:
+ try:
+ subcategory = unhexlify(name)
+ except:
+ raise cherrypy.HTTPError(404, 'Invalid category id: %r'%name)
+ 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 = {}
+
+ 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
+ children = set()
+
+ 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':hexlify(toplevel+'.'+x),
+ 'icon':category_icon(toplevel, meta)} for x in children]
+
+ for x in subcategories:
+ x['url'] = category_url(self.opts.url_prefix, x['url'])
+ x['icon'] = icon_url(self.opts.url_prefix, x['icon'])
+
+ 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': absurl(self.opts.url_prefix, '/ajax/books_in/%s/%s'%(
+ hexlify(toplevel), hexlify(x.original_name))),
+ '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':num, 'sort':sort,
+ 'sort_order':sort_order,
+ 'subcategories':subcategories,
+ 'items':items,
+ }
+
+
+ # }}}
+
+ # Books in the specified category {{{
+ @Endpoint()
+ def ajax_books_in(self, name, item, sort='title', num=100, offset=0,
+ sort_order='asc'):
pass
+ # }}}
+