diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py
index 49f4e076a4..380bdbf518 100644
--- a/src/calibre/ebooks/mobi/writer.py
+++ b/src/calibre/ebooks/mobi/writer.py
@@ -296,9 +296,11 @@ class Serializer(object):
class MobiWriter(object):
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
- def __init__(self, compression=None, imagemax=None):
+ def __init__(self, compression=None, imagemax=None,
+ prefer_author_sort=False):
self._compression = compression or UNCOMPRESSED
self._imagemax = imagemax or OTHER_MAX_IMAGE_SIZE
+ self._prefer_author_sort = prefer_author_sort
def dump(self, oeb, path):
if hasattr(path, 'write'):
@@ -457,12 +459,19 @@ class MobiWriter(object):
for term in oeb.metadata:
if term not in EXTH_CODES: continue
code = EXTH_CODES[term]
- for item in oeb.metadata[term]:
+ items = oeb.metadata[term]
+ if term == 'creator':
+ if self._prefer_author_sort:
+ creators = [unicode(c.file_as or c) for c in items]
+ else:
+ creators = [unicode(c) for c in items]
+ items = ['; '.join(creators)]
+ for item in items:
data = self.COLLAPSE_RE.sub(' ', unicode(item))
if term == 'identifier':
if data.lower().startswith('urn:isbn:'):
data = data[9:]
- elif item.get('scheme', '').lower() == 'isbn':
+ elif item.scheme.lower() == 'isbn':
pass
else:
continue
@@ -535,6 +544,9 @@ def config(defaults=None):
help=_('Render HTML tables as blocks of text instead of actual '
'tables. This is neccessary if the HTML contains very large '
'or complex tables.'))
+ mobi('prefer_author_sort', ['--prefer-author-sort'], default=False,
+ help=_('When present, use the author sorting information for '
+ 'generating the Mobipocket author metadata.'))
profiles = c.add_group('profiles', _('Device renderer profiles. '
'Affects conversion of font sizes, image rescaling and rasterization '
'of tables. Valid profiles are: %s.') % ', '.join(_profiles))
@@ -594,7 +606,8 @@ def oeb2mobi(opts, inpath):
trimmer.transform(oeb, context)
mobimlizer = MobiMLizer(ignore_tables=opts.ignore_tables)
mobimlizer.transform(oeb, context)
- writer = MobiWriter(compression=compression, imagemax=imagemax)
+ writer = MobiWriter(compression=compression, imagemax=imagemax,
+ prefer_author_sort=opts.prefer_author_sort)
writer.dump(oeb, outpath)
run_plugins_on_postprocess(outpath, 'mobi')
logger.info(_('Output written to ') + outpath)
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index 106a091c71..b89be6b1ec 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -52,6 +52,8 @@ def XML(name): return '{%s}%s' % (XML_NS, name)
def XHTML(name): return '{%s}%s' % (XHTML_NS, name)
def OPF(name): return '{%s}%s' % (OPF2_NS, name)
def DC(name): return '{%s}%s' % (DC11_NS, name)
+def XSI(name): return '{%s}%s' % (XSI_NS, name)
+def DCTERMS(name): return '{%s}%s' % (DCTERMS_NS, name)
def NCX(name): return '{%s}%s' % (NCX_NS, name)
def SVG(name): return '{%s}%s' % (SVG_NS, name)
def XLINK(name): return '{%s}%s' % (XLINK_NS, name)
@@ -211,16 +213,44 @@ class DirWriter(object):
class Metadata(object):
- DC_TERMS = set(['contributor', 'coverage', 'creator', 'date', 'description',
- 'format', 'identifier', 'language', 'publisher', 'relation',
- 'rights', 'source', 'subject', 'title', 'type'])
+ DC_TERMS = set(['contributor', 'coverage', 'creator', 'date',
+ 'description', 'format', 'identifier', 'language',
+ 'publisher', 'relation', 'rights', 'source', 'subject',
+ 'title', 'type'])
CALIBRE_TERMS = set(['series', 'series_index', 'rating'])
- OPF_ATTRS = set(['role', 'file-as', 'scheme', 'event'])
+ OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'),
+ 'scheme': OPF('scheme'), 'event': OPF('event'),
+ 'type': XSI('type'), 'lang': XML('lang'), 'id': 'id'}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
'xsi': XSI_NS, 'calibre': CALIBRE_NS}
class Item(object):
+ class Attribute(object):
+ def __init__(self, attr, allowed=None):
+ if not callable(attr):
+ attr_, attr = attr, lambda term: attr_
+ self.attr = attr
+ self.allowed = allowed
+
+ def term_attr(self, obj):
+ term = obj.term
+ if namespace(term) != DC11_NS:
+ term = OPF('meta')
+ allowed = self.allowed
+ if allowed is not None and term not in allowed:
+ raise AttributeError(
+ 'attribute %r not valid for metadata term %r' \
+ % (self.attr(term), barename(obj.term)))
+ return self.attr(term)
+
+ def __get__(self, obj, cls):
+ if obj is None: return None
+ return obj.attrib.get(self.term_attr(obj), '')
+
+ def __set__(self, obj, value):
+ obj.attrib[self.term_attr(obj)] = value
+
def __init__(self, term, value, attrib={}, nsmap={}, **kwargs):
self.attrib = attrib = dict(attrib)
self.nsmap = nsmap = dict(nsmap)
@@ -240,30 +270,29 @@ class Metadata(object):
for attr, value in attrib.items():
if isprefixname(value):
attrib[attr] = qname(value, nsmap)
- if attr in Metadata.OPF_ATTRS:
- attrib[OPF(attr)] = attrib.pop(attr)
- self.__setattr__ = self._setattr
+ nsattr = Metadata.OPF_ATTRS.get(attr, attr)
+ if nsattr == OPF('scheme') and namespace(term) != DC11_NS:
+ # The opf:meta element takes @scheme, not @opf:scheme
+ nsattr = 'scheme'
+ if attr != nsattr:
+ attrib[nsattr] = attrib.pop(attr)
+
+ def scheme(term):
+ if term == OPF('meta'):
+ return 'scheme'
+ return OPF('scheme')
+ scheme = Attribute(scheme, [DC('identifier'), OPF('meta')])
+ file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor')])
+ role = Attribute(OPF('role'), [DC('creator'), DC('contributor')])
+ event = Attribute(OPF('event'), [DC('date')])
+ id = Attribute('id')
+ type = Attribute(XSI('type'), [DC('date'), DC('format'), DC('type')])
+ lang = Attribute(XML('lang'), [DC('contributor'), DC('coverage'),
+ DC('creator'), DC('publisher'),
+ DC('relation'), DC('rights'),
+ DC('source'), DC('subject'),
+ OPF('meta')])
- def __getattr__(self, name):
- attr = name.replace('_', '-')
- if attr in Metadata.OPF_ATTRS:
- attr = OPF(attr)
- try:
- return self.attrib[attr]
- except KeyError:
- raise AttributeError(
- '%r object has no attribute %r' \
- % (self.__class__.__name__, name))
-
- def _setattr(self, name, value):
- attr = name.replace('_', '-')
- if attr in Metadata.OPF_ATTRS:
- attr = OPF(attr)
- if attr in self.attrib:
- self.attrib[attr] = value
- return
- super(Item, self).__setattr__(self, name, value)
-
def __getitem__(self, key):
return self.attrib[key]
diff --git a/src/calibre/gui2/dialogs/epub.py b/src/calibre/gui2/dialogs/epub.py
index 614f18e5b4..1e38e4d879 100644
--- a/src/calibre/gui2/dialogs/epub.py
+++ b/src/calibre/gui2/dialogs/epub.py
@@ -62,6 +62,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.toc_title_label.setVisible(False)
self.opt_rescale_images.setVisible(False)
self.opt_ignore_tables.setVisible(False)
+ self.opt_prefer_author_sort.setVisible(False)
def initialize(self):
self.__w = []
diff --git a/src/calibre/gui2/dialogs/epub.ui b/src/calibre/gui2/dialogs/epub.ui
index a346bed4e8..d0ff834309 100644
--- a/src/calibre/gui2/dialogs/epub.ui
+++ b/src/calibre/gui2/dialogs/epub.ui
@@ -105,6 +105,36 @@
Book Cover
+ -
+
+
-
+
+
+
+
+
+ :/images/book.svg
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+ Use cover from &source file
+
+
+ true
+
+
+
-
@@ -156,36 +186,6 @@
- -
-
-
- Use cover from &source file
-
-
- true
-
-
-
- -
-
-
-
-
-
-
-
-
- :/images/book.svg
-
-
- true
-
-
- Qt::AlignCenter
-
-
-
-
-
opt_prefer_metadata_cover
@@ -486,6 +486,13 @@
+ -
+
+
+ &Use author sort to set author field in output
+
+
+
-
diff --git a/src/calibre/gui2/dialogs/job_view.ui b/src/calibre/gui2/dialogs/job_view.ui
index ffd4419da9..f4b0086497 100644
--- a/src/calibre/gui2/dialogs/job_view.ui
+++ b/src/calibre/gui2/dialogs/job_view.ui
@@ -16,7 +16,7 @@
:/images/view.svg:/images/view.svg
-
+
-
@@ -30,10 +30,34 @@
+ -
+
+
+ QDialogButtonBox::Ok
+
+
+
-
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 617
+ 442
+
+
+ 206
+ -5
+
+
+
+
diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py
index 0e426aaf42..2b0148a194 100644
--- a/src/calibre/gui2/library.py
+++ b/src/calibre/gui2/library.py
@@ -575,6 +575,7 @@ class BooksModel(QAbstractTableModel):
if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
+ self.db.set_rating(id, val)
elif column == 'series':
pat = re.compile(r'\[(\d+)\]')
match = pat.search(val)
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index ffd4c46d7f..edcfa99342 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -485,14 +485,14 @@ def post_install():
os.unlink(f)
def binary_install():
- manifest = os.path.join(sys.frozen_path, 'manifest')
+ manifest = os.path.join(getattr(sys, 'frozen_path'), 'manifest')
exes = [x.strip() for x in open(manifest).readlines()]
print 'Creating symlinks...'
for exe in exes:
dest = os.path.join('/usr', 'bin', exe)
if os.path.exists(dest):
os.remove(dest)
- os.symlink(os.path.join(sys.frozen_path, exe), dest)
+ os.symlink(os.path.join(getattr(sys, 'frozen_path'), exe), dest)
post_install()
return 0
diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py
index 8dee540e01..f5ffaf08b8 100644
--- a/src/calibre/web/fetch/simple.py
+++ b/src/calibre/web/fetch/simple.py
@@ -139,6 +139,8 @@ class RecursiveFetcher(object, LoggingInterface):
if self.keep_only_tags:
body = Tag(soup, 'body')
try:
+ if isinstance(self.keep_only_tags, dict):
+ self.keep_only_tags = [self.keep_only_tags]
for spec in self.keep_only_tags:
for tag in soup.find('body').findAll(**spec):
body.insert(len(body.contents), tag)