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)