+ '''.format(
+ xml(_('Browsing by')+': ' + category_name), items,
+ xml(_('Up'), True))
+
+ return self.browse_template(sort).format(title=category_name,
+ script=script, main=main)
+
+ @Endpoint(mimetype='application/json; charset=utf-8')
+ def browse_category_group(self, category=None, group=None,
+ category_sort=None):
+ sort = category_sort
+ if sort not in ('rating', 'name', 'popularity'):
+ sort = 'name'
+ categories = self.categories_cache()
+ if category not in categories:
+ raise cherrypy.HTTPError(404, 'category not found')
+
+ category_meta = self.db.field_metadata
+ datatype = category_meta[category]['datatype']
+
+ if not group:
+ raise cherrypy.HTTPError(404, 'invalid group')
+
+ items = categories[category]
+ entries = []
+ getter = lambda x: unicode(getattr(x, 'sort', 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, self.db, datatype)
+ return json.dumps(entries, ensure_ascii=False)
+
+
+
+ @Endpoint()
+ def browse_catalog(self, category=None, category_sort=None):
+ 'Entry point for top-level, categories and sub-categories'
+ if category == None:
+ ans = self.browse_toplevel()
+ elif category == 'newest':
+ raise cherrypy.InternalRedirect('/browse/matches/newest/dummy')
+ else:
+ ans = self.browse_category(category, category_sort)
+
return ans
# }}}
# Book Lists {{{
- def browse_list(self, query=None):
- raise NotImplementedError()
+
+ 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')
+ self.sort(items, sort, ascending)
+ return sort
+
+ @Endpoint(sort_type='list')
+ def browse_matches(self, category=None, cid=None, list_sort=None):
+ if not cid:
+ raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid)
+ categories = self.categories_cache()
+
+ if category not in categories and category != 'newest':
+ raise cherrypy.HTTPError(404, 'category not found')
+ try:
+ category_name = self.db.field_metadata[category]['name']
+ except:
+ if category != 'newest':
+ raise
+ category_name = _('Newest')
+
+ if category == 'search':
+ which = unhexlify(cid)
+ try:
+ ids = self.search_cache('search:"%s"'%which)
+ except:
+ raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
+ elif category == 'newest':
+ ids = list(self.db.data.iterallids())
+ else:
+ ids = self.db.get_books_for_category(category, cid)
+
+ items = [self.db.data._data[x] for x in ids]
+ if category == 'newest':
+ list_sort = 'timestamp'
+ sort = self.browse_sort_book_list(items, list_sort)
+ ids = [x[0] for x in items]
+ html = render_book_list(ids)
+ return self.browse_template(sort).format(
+ title=_('Books in') + " " +category_name,
+ script='booklist();', main=html)
+
+ @Endpoint(mimetype='application/json; charset=utf-8', sort_type='list')
+ def browse_booklist_page(self, ids=None, list_sort=None):
+ if ids is None:
+ ids = json.dumps('[]')
+ try:
+ ids = json.loads(ids)
+ except:
+ raise cherrypy.HTTPError(404, 'invalid ids')
+
# }}}
# Search {{{
- def browse_search(self, query=None):
+ def browse_search(self, query=None, offset=0, sort=None):
raise NotImplementedError()
# }}}
@@ -87,8 +414,4 @@ class BrowseServer(object):
raise NotImplementedError()
# }}}
- # JSON {{{
- def browse_json(self, query=None):
- raise NotImplementedError()
- # }}}
diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py
index 94e4a1c041..29602a114c 100644
--- a/src/calibre/library/server/cache.py
+++ b/src/calibre/library/server/cache.py
@@ -29,6 +29,11 @@ class Cache(object):
def categories_cache(self, restrict_to=frozenset([])):
+ base_restriction = self.search_cache('')
+ if restrict_to:
+ restrict_to = frozenset(restrict_to).intersection(base_restriction)
+ else:
+ restrict_to = base_restriction
old = self._category_cache.pop(frozenset(restrict_to), None)
if old is None or old[0] <= self.db.last_modified():
categories = self.db.get_categories(ids=restrict_to)
diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py
index 7139b12d08..8c5fef4ee1 100644
--- a/src/calibre/library/server/content.py
+++ b/src/calibre/library/server/content.py
@@ -5,18 +5,15 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import re, os, cStringIO
+import re, os
import cherrypy
-try:
- from PIL import Image as PILImage
- PILImage
-except ImportError:
- import Image as PILImage
from calibre import fit_image, guess_type
from calibre.utils.date import fromtimestamp
from calibre.library.caches import SortKeyGenerator
+from calibre.utils.magick.draw import save_cover_data_to, Image, \
+ thumbnail as generate_thumbnail
class CSSortKeyGenerator(SortKeyGenerator):
@@ -35,6 +32,7 @@ class ContentServer(object):
def add_routes(self, connect):
connect('root', '/', self.index)
+ connect('old', '/old', self.old)
connect('get', '/get/{what}/{id}', self.get,
conditions=dict(method=["GET", "HEAD"]))
connect('static', '/static/{name:.*?}', self.static,
@@ -76,8 +74,13 @@ class ContentServer(object):
id = int(match.group())
if not self.db.has_id(id):
raise cherrypy.HTTPError(400, 'id:%d does not exist in database'%id)
- if what == 'thumb':
- return self.get_cover(id, thumbnail=True)
+ if what == 'thumb' or what.startswith('thumb_'):
+ try:
+ width, height = map(int, what.split('_')[1:])
+ except:
+ width, height = 60, 80
+ return self.get_cover(id, thumbnail=True, thumb_width=width,
+ thumb_height=height)
if what == 'cover':
return self.get_cover(id)
return self.get_format(id, what)
@@ -123,38 +126,43 @@ class ContentServer(object):
return self.static('index.html')
+ def old(self, **kwargs):
+ return self.static('index.html')
+
# Actually get content from the database {{{
- def get_cover(self, id, thumbnail=False):
- cover = self.db.cover(id, index_is_id=True, as_file=False)
- if cover is None:
- cover = self.default_cover
- cherrypy.response.headers['Content-Type'] = 'image/jpeg'
- cherrypy.response.timeout = 3600
- path = getattr(cover, 'name', False)
- updated = fromtimestamp(os.stat(path).st_mtime) if path and \
- os.access(path, os.R_OK) else self.build_time
- cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
+ def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80):
try:
- f = cStringIO.StringIO(cover)
- try:
- im = PILImage.open(f)
- except IOError:
- raise cherrypy.HTTPError(404, 'No valid cover found')
- width, height = im.size
+ cherrypy.response.headers['Content-Type'] = 'image/jpeg'
+ cherrypy.response.timeout = 3600
+ cover = self.db.cover(id, index_is_id=True, as_file=True)
+ if cover is None:
+ cover = self.default_cover
+ updated = self.build_time
+ else:
+ with cover as f:
+ updated = fromtimestamp(os.stat(f.name).st_mtime)
+ cover = f.read()
+ cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
+
+ if thumbnail:
+ return generate_thumbnail(cover,
+ width=thumb_width, height=thumb_height)[-1]
+
+ img = Image()
+ img.load(cover)
+ width, height = img.size
scaled, width, height = fit_image(width, height,
- 60 if thumbnail else self.max_cover_width,
- 80 if thumbnail else self.max_cover_height)
+ thumb_width if thumbnail else self.max_cover_width,
+ thumb_height if thumbnail else self.max_cover_height)
if not scaled:
return cover
- im = im.resize((int(width), int(height)), PILImage.ANTIALIAS)
- of = cStringIO.StringIO()
- im.convert('RGB').save(of, 'JPEG')
- return of.getvalue()
+ return save_cover_data_to(img, 'img.jpg', return_data=True,
+ resize_to=(width, height))
except Exception, err:
import traceback
cherrypy.log.error('Failed to generate cover:')
cherrypy.log.error(traceback.print_exc())
- raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
+ raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err)
def get_format(self, id, format):
format = format.upper()
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index f1aeb583db..16e7d34cbf 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -18,7 +18,7 @@ from calibre.constants import __appname__
from calibre.ebooks.metadata import fmt_sidx
from calibre.library.comments import comments_to_html
from calibre.library.server import custom_fields_to_display
-from calibre.library.server.utils import format_tag_string
+from calibre.library.server.utils import format_tag_string, Offsets
from calibre import guess_type
from calibre.utils.ordered_dict import OrderedDict
@@ -321,26 +321,6 @@ class CategoryGroupFeed(NavFeed):
self.root.append(CATALOG_GROUP_ENTRY(item, which, base_href, version, updated))
-class OPDSOffsets(object):
-
- def __init__(self, offset, delta, total):
- if offset < 0:
- offset = 0
- if offset >= total:
- raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
- last_allowed_index = total - 1
- last_current_index = offset + delta - 1
- 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
-
class OPDSServer(object):
@@ -374,7 +354,7 @@ class OPDSServer(object):
items = [x for x in self.db.data.iterall() if x[idx] in ids]
self.sort(items, sort_by, ascending)
max_items = self.opts.max_opds_items
- offsets = OPDSOffsets(offset, max_items, len(items))
+ offsets = Offsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.offset+max_items]
updated = self.db.last_modified()
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
@@ -448,7 +428,7 @@ class OPDSServer(object):
id_ = 'calibre-category-group-feed:'+category+':'+which
max_items = self.opts.max_opds_items
- offsets = OPDSOffsets(offset, max_items, len(items))
+ offsets = Offsets(offset, max_items, len(items))
items = list(items)[offsets.offset:offsets.offset+max_items]
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
@@ -495,7 +475,7 @@ class OPDSServer(object):
if len(items) <= MAX_ITEMS:
max_items = self.opts.max_opds_items
- offsets = OPDSOffsets(offset, max_items, len(items))
+ offsets = Offsets(offset, max_items, len(items))
items = list(items)[offsets.offset:offsets.offset+max_items]
ans = CategoryFeed(items, which, id_, updated, version, offsets,
page_url, up_url, self.db)
@@ -516,7 +496,7 @@ class OPDSServer(object):
getattr(y, 'sort', y.name).startswith(x)])
items = [Group(x, y) for x, y in category_groups.items()]
max_items = self.opts.max_opds_items
- offsets = OPDSOffsets(offset, max_items, len(items))
+ offsets = Offsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.offset+max_items]
ans = CategoryGroupFeed(items, which, id_, updated, version, offsets,
page_url, up_url)
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index 9a64948a3d..35c92f7ae2 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -13,6 +13,28 @@ from calibre import strftime as _strftime, prints
from calibre.utils.date import now as nowf
from calibre.utils.config import tweaks
+class Offsets(object):
+ 'Calculate offsets for a paginated view'
+
+ def __init__(self, offset, delta, total):
+ if offset < 0:
+ offset = 0
+ if offset >= total:
+ raise cherrypy.HTTPError(404, '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
+
def expose(func):
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 3cf171bc1b..359cc4755f 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -387,6 +387,12 @@ solve it, look for a corrupted font file on your system, in ~/Library/Fonts or t
check for corrupted fonts in OS X is to start the "Font Book" application, select all fonts and then in the File
menu, choose "Validate fonts".
+
+I downloaded the installer, but it is not working?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums `_.
+
My antivirus program claims |app| is a virus/trojan?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -418,3 +424,14 @@ How do I run calibre from my USB stick?
A portable version of calibre is available at: `portableapps.com `_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions `.
+Why are there so many calibre-parallel processes on my system?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+|app| maintains two separate worker process pools. One is used for adding books/saving to disk and the other for conversions. You can control the number of worker processes via :guilabel:`Preferences->Advanced->Miscellaneous`. So if you set it to 6 that means a maximum of 3 conversions will run simultaneously. And that is why you will see the number of worker processes changes by two when you use the up and down arrows. On windows, you can set the priority that these processes run with. This can be useful on older, single CPU machines, if you find them slowing down to a crawl when conversions are running.
+
+In addition to this some conversion plugins run tasks in their own pool of processes, so for example if you bulk convert comics, each comic conversion will use three separate processes to render the images. The job manager knows this so it will run only a single comic conversion simultaneously.
+
+And since I'm sure someone will ask: The reason adding/saving books are in separate processes is because of PDF. PDF processing libraries can crash on reading PDFs and I dont want the crash to take down all of calibre. Also when adding EPUB books, in order to extract the cover you have to sometimes render the HTML of the first page, which means that it either has to run the GUI thread of the main process or in a separate process.
+
+Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes.
+
diff --git a/src/calibre/utils/ipc/launch.py b/src/calibre/utils/ipc/launch.py
index 8d3628d69a..a179f356be 100644
--- a/src/calibre/utils/ipc/launch.py
+++ b/src/calibre/utils/ipc/launch.py
@@ -14,7 +14,10 @@ from calibre.ptempfile import PersistentTemporaryFile, base_dir
if iswindows:
import win32process
- _windows_null_file = open(os.devnull, 'wb')
+ try:
+ _windows_null_file = open(os.devnull, 'wb')
+ except:
+ raise RuntimeError('NUL %r file missing in windows'%os.devnull)
class Worker(object):
'''
diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py
index 6808215554..5c978a27e0 100644
--- a/src/calibre/utils/magick/draw.py
+++ b/src/calibre/utils/magick/draw.py
@@ -25,6 +25,7 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
resize and the input and output image formats are the same, no changes are
made.
+ :param data: Image data as bytestring or Image object
:param compression_quality: The quality of the image after compression.
Number between 1 and 100. 1 means highest compression, 100 means no
compression (lossless).
@@ -33,8 +34,11 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
'''
changed = False
- img = Image()
- img.load(data)
+ if isinstance(data, Image):
+ img = data
+ else:
+ img = Image()
+ img.load(data)
orig_fmt = normalize_format_name(img.format)
fmt = os.path.splitext(path)[1]
fmt = normalize_format_name(fmt[1:])
diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py
index 230a983b74..b8b46a96cb 100644
--- a/src/calibre/utils/smtp.py
+++ b/src/calibre/utils/smtp.py
@@ -11,6 +11,7 @@ This module implements a simple commandline SMTP client that supports:
import sys, traceback, os
from email import encoders
+from calibre import isbytestring
def create_mail(from_, to, subject, text=None, attachment_data=None,
attachment_type=None, attachment_name=None):
@@ -26,7 +27,10 @@ def create_mail(from_, to, subject, text=None, attachment_data=None,
if text is not None:
from email.mime.text import MIMEText
- msg = MIMEText(text)
+ if isbytestring(text):
+ msg = MIMEText(text)
+ else:
+ msg = MIMEText(text, 'plain', 'utf-8')
outer.attach(msg)
if attachment_data is not None: