diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index f6f8c83666..b8940af5b5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -421,6 +421,16 @@ class EPUBMetadataWriter(MetadataWriterPlugin): from calibre.ebooks.metadata.epub import set_metadata set_metadata(stream, mi, apply_null=self.apply_null) +class FB2MetadataWriter(MetadataWriterPlugin): + + name = 'Set FB2 metadata' + file_types = set(['fb2']) + description = _('Set metadata in %s files')%'FB2' + + def set_metadata(self, stream, mi, type): + from calibre.ebooks.metadata.fb2 import set_metadata + set_metadata(stream, mi, apply_null=self.apply_null) + class HTMLZMetadataWriter(MetadataWriterPlugin): name = 'Set HTMLZ metadata' diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py index 2bcbe931b8..552a762cb2 100644 --- a/src/calibre/ebooks/metadata/fb2.py +++ b/src/calibre/ebooks/metadata/fb2.py @@ -22,6 +22,9 @@ NAMESPACES = { XPath = partial(etree.XPath, namespaces=NAMESPACES) tostring = partial(etree.tostring, method='text', encoding=unicode) +def FB2(tag): + return '{%s}%s'%(NAMESPACES['fb2'], tag) + def get_metadata(stream): ''' Return fb2 metadata as a L{MetaInformation} object ''' @@ -85,6 +88,7 @@ def _parse_authors(root): authors = [] # pick up authors but only from 1 secrion ; otherwise it is not consistent! # Those are fallbacks: , + author = None for author_sec in ['title-info', 'src-title-info', 'document-info']: for au in XPath('//fb2:%s/fb2:author'%author_sec)(root): author = _parse_author(au) @@ -211,8 +215,8 @@ def _parse_publisher(root, mi): def _parse_pubdate(root, mi): year = XPath('number(//fb2:publish-info/fb2:year/text())')(root) if float.is_integer(year): - # only year is available, so use 1-st of Jan - mi.pubdate = datetime.date(int(year), 1, 1) + # only year is available, so use 2nd of June + mi.pubdate = datetime.date(int(year), 6, 2) def _parse_timestamp(root, mi): #03.12.1996 @@ -239,3 +243,93 @@ def _get_fbroot(stream): raw = xml_to_unicode(raw, strip_encoding_pats=True)[0] root = etree.fromstring(raw, parser=parser) return root + +def _clear_meta_tags(doc, tag): + for parent in ('title-info', 'src-title-info', 'publish-info'): + for x in XPath('//fb2:%s/fb2:%s'%(parent, tag))(doc): + x.getparent().remove(x) + +def _set_title(title_info, mi): + if not mi.is_null('title'): + _clear_meta_tags(title_info, 'book-title') + title = _get_or_create(title_info, 'book-title') + title.text = mi.title + +def _set_comments(title_info, mi): + if not mi.is_null('comments'): + _clear_meta_tags(title_info, 'annotation') + title = _get_or_create(title_info, 'annotation') + title.text = mi.comments + + +def _set_authors(title_info, mi): + if not mi.is_null('authors'): + _clear_meta_tags(title_info, 'author') + for author in mi.authors: + author_parts = author.split() + if not author_parts: continue + atag = title_info.makeelement(FB2('author')) + title_info.insert(0, atag) + if len(author_parts) == 1: + _get_or_create(atag, 'nickname').text = author + else: + _get_or_create(atag, 'first-name').text = author_parts[0] + author_parts = author_parts[1:] + if len(author_parts) > 1: + _get_or_create(atag, 'middle-name', at_start=False).text = author_parts[0] + author_parts = author_parts[1:] + if author_parts: + _get_or_create(atag, 'last-name', at_start=False).text = ' '.join(author_parts) + +def _set_tags(title_info, mi): + if not mi.is_null('tags'): + _clear_meta_tags(title_info, 'genre') + for t in mi.tags: + tag = title_info.makeelement(FB2('genre')) + tag.text = t + title_info.insert(0, tag) + +def _set_series(title_info, mi): + if not mi.is_null('series'): + _clear_meta_tags(title_info, 'sequence') + seq = _get_or_create(title_info, 'sequence') + seq.set('name', mi.series) + try: + seq.set('number', '%g'%mi.series_index) + except: + seq.set('number', '1') + +def _get_or_create(parent, tag, at_start=True): + ans = XPath('./fb2:'+tag)(parent) + if ans: + ans = ans[0] + else: + ans = parent.makeelement(FB2(tag)) + if at_start: + parent.insert(0, ans) + else: + parent.append(ans) + return ans + +def set_metadata(stream, mi, apply_null=False, update_timestamp=False): + stream.seek(0) + root = _get_fbroot(stream) + desc = _get_or_create(root, 'description') + ti = _get_or_create(desc, 'title-info') + + indent = ti.text + + _set_comments(ti, mi) + _set_series(ti, mi) + _set_tags(ti, mi) + _set_authors(ti, mi) + _set_title(ti, mi) + + for child in ti: + child.tail = indent + + stream.seek(0) + stream.truncate() + stream.write(etree.tostring(root, method='xml', encoding='utf-8', + xml_declaration=True)) +