~ubuntu-branches/ubuntu/karmic/calibre/karmic

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/metadata/opf2.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
lxml based OPF parser.
8
8
'''
9
9
 
10
 
import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
 
10
import re, sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
11
11
from urllib import unquote
12
12
from urlparse import urlparse
13
13
 
15
15
from dateutil import parser
16
16
 
17
17
from calibre.ebooks.chardet import xml_to_unicode
18
 
from calibre import relpath
19
 
from calibre.constants import __appname__, __version__
 
18
from calibre.constants import __appname__, __version__, filesystem_encoding
20
19
from calibre.ebooks.metadata.toc import TOC
21
20
from calibre.ebooks.metadata import MetaInformation, string_to_authors
22
21
 
86
85
        if self.path == basedir:
87
86
            return ''+frag
88
87
        try:
89
 
            rpath = relpath(self.path, basedir)
90
 
        except OSError: # On windows path and basedir could be on different drives
 
88
            rpath = os.path.relpath(self.path, basedir)
 
89
        except ValueError: # On windows path and basedir could be on different drives
91
90
            rpath = self.path
92
91
        if isinstance(rpath, unicode):
93
92
            rpath = rpath.encode('utf-8')
169
168
                res.mime_type = mt
170
169
            return res
171
170
 
172
 
    @apply
173
 
    def media_type():
 
171
    @dynamic_property
 
172
    def media_type(self):
174
173
        def fget(self):
175
174
            return self.mime_type
176
175
        def fset(self, val):
387
386
                ans = self.formatter(ans)
388
387
            except:
389
388
                return None
 
389
        if hasattr(ans, 'strip'):
 
390
            ans = ans.strip()
390
391
        return ans
391
392
 
392
393
    def __get__(self, obj, type=None):
434
435
    spine_path      = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
435
436
    guide_path      = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]')
436
437
 
437
 
    title           = MetadataField('title')
 
438
    title           = MetadataField('title', formatter=lambda x: re.sub(r'\s+', ' ', x))
438
439
    publisher       = MetadataField('publisher')
439
440
    language        = MetadataField('language')
440
441
    comments        = MetadataField('description')
441
 
    category        = MetadataField('category')
 
442
    category        = MetadataField('type')
 
443
    rights          = MetadataField('rights')
442
444
    series          = MetadataField('series', is_dc=False)
443
 
    series_index    = MetadataField('series_index', is_dc=False, formatter=int, none_is=1)
 
445
    series_index    = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
444
446
    rating          = MetadataField('rating', is_dc=False, formatter=int)
445
 
    timestamp       = MetadataField('date', formatter=parser.parse)
 
447
    pubdate         = MetadataField('date', formatter=parser.parse)
 
448
    publication_type = MetadataField('publication_type', is_dc=False)
 
449
    timestamp       = MetadataField('timestamp', is_dc=False, formatter=parser.parse)
446
450
 
447
451
 
448
452
    def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True):
449
453
        if not hasattr(stream, 'read'):
450
454
            stream = open(stream, 'rb')
 
455
        raw = stream.read()
 
456
        if not raw:
 
457
            raise ValueError('Empty file: '+getattr(stream, 'name', 'stream'))
451
458
        self.basedir  = self.base_dir = basedir
452
459
        self.path_to_html_toc = self.html_toc_fragment = None
453
 
        raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
 
460
        raw, self.encoding = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)
454
461
        raw = raw[raw.find('<'):]
455
462
        self.root     = etree.fromstring(raw, self.PARSER)
456
463
        self.metadata = self.metadata_path(self.root)
557
564
                has_path = True
558
565
                break
559
566
        if not has_path:
560
 
            href = relpath(path, self.base_dir).replace(os.sep, '/')
 
567
            href = os.path.relpath(path, self.base_dir).replace(os.sep, '/')
561
568
            item = self.create_manifest_item(href, media_type)
562
569
            manifest = self.manifest_ppath(self.root)[0]
563
570
            manifest.append(item)
620
627
        for item in self.iterguide():
621
628
            item.set('href', get_href(item))
622
629
 
623
 
    @apply
624
 
    def authors():
 
630
    @dynamic_property
 
631
    def authors(self):
625
632
 
626
633
        def fget(self):
627
634
            ans = []
640
647
 
641
648
        return property(fget=fget, fset=fset)
642
649
 
643
 
    @apply
644
 
    def author_sort():
645
 
 
646
 
        def fget(self):
647
 
            matches = self.authors_path(self.metadata)
648
 
            if matches:
649
 
                for match in matches:
650
 
                    ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
651
 
                    if not ans:
652
 
                        ans = match.get('file-as', None)
653
 
                    if ans:
654
 
                        return ans
655
 
 
656
 
        def fset(self, val):
657
 
            matches = self.authors_path(self.metadata)
658
 
            if matches:
659
 
                for key in matches[0].attrib:
660
 
                    if key.endswith('file-as'):
661
 
                        matches[0].attrib.pop(key)
662
 
                matches[0].set('file-as', unicode(val))
663
 
 
664
 
        return property(fget=fget, fset=fset)
665
 
 
666
 
    @apply
667
 
    def title_sort():
668
 
 
669
 
        def fget(self):
670
 
            matches = self.title_path(self.metadata)
671
 
            if matches:
672
 
                for match in matches:
673
 
                    ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
674
 
                    if not ans:
675
 
                        ans = match.get('file-as', None)
676
 
                    if ans:
677
 
                        return ans
678
 
 
679
 
        def fset(self, val):
680
 
            matches = self.title_path(self.metadata)
681
 
            if matches:
682
 
                for key in matches[0].attrib:
683
 
                    if key.endswith('file-as'):
684
 
                        matches[0].attrib.pop(key)
685
 
                matches[0].set('file-as', unicode(val))
686
 
 
687
 
        return property(fget=fget, fset=fset)
688
 
 
689
 
    @apply
690
 
    def tags():
 
650
    @dynamic_property
 
651
    def author_sort(self):
 
652
 
 
653
        def fget(self):
 
654
            matches = self.authors_path(self.metadata)
 
655
            if matches:
 
656
                for match in matches:
 
657
                    ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
 
658
                    if not ans:
 
659
                        ans = match.get('file-as', None)
 
660
                    if ans:
 
661
                        return ans
 
662
 
 
663
        def fset(self, val):
 
664
            matches = self.authors_path(self.metadata)
 
665
            if matches:
 
666
                for key in matches[0].attrib:
 
667
                    if key.endswith('file-as'):
 
668
                        matches[0].attrib.pop(key)
 
669
                matches[0].set('file-as', unicode(val))
 
670
 
 
671
        return property(fget=fget, fset=fset)
 
672
 
 
673
    @dynamic_property
 
674
    def title_sort(self):
 
675
 
 
676
        def fget(self):
 
677
            matches = self.title_path(self.metadata)
 
678
            if matches:
 
679
                for match in matches:
 
680
                    ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
 
681
                    if not ans:
 
682
                        ans = match.get('file-as', None)
 
683
                    if ans:
 
684
                        return ans
 
685
 
 
686
        def fset(self, val):
 
687
            matches = self.title_path(self.metadata)
 
688
            if matches:
 
689
                for key in matches[0].attrib:
 
690
                    if key.endswith('file-as'):
 
691
                        matches[0].attrib.pop(key)
 
692
                matches[0].set('file-as', unicode(val))
 
693
 
 
694
        return property(fget=fget, fset=fset)
 
695
 
 
696
    @dynamic_property
 
697
    def tags(self):
691
698
 
692
699
        def fget(self):
693
700
            ans = []
704
711
 
705
712
        return property(fget=fget, fset=fset)
706
713
 
707
 
    @apply
708
 
    def isbn():
 
714
    @dynamic_property
 
715
    def isbn(self):
709
716
 
710
717
        def fget(self):
711
718
            for match in self.isbn_path(self.metadata):
721
728
 
722
729
        return property(fget=fget, fset=fset)
723
730
 
724
 
    @apply
725
 
    def application_id():
 
731
    @dynamic_property
 
732
    def application_id(self):
726
733
 
727
734
        def fget(self):
728
735
            for match in self.application_id_path(self.metadata):
738
745
 
739
746
        return property(fget=fget, fset=fset)
740
747
 
741
 
    @apply
742
 
    def book_producer():
 
748
    @dynamic_property
 
749
    def book_producer(self):
743
750
 
744
751
        def fget(self):
745
752
            for match in self.bkp_path(self.metadata):
776
783
                            return cpath
777
784
 
778
785
 
779
 
    @apply
780
 
    def cover():
 
786
    @dynamic_property
 
787
    def cover(self):
781
788
 
782
789
        def fget(self):
783
790
            if self.guide is not None:
922
929
        self.guide.set_basedir(self.base_path)
923
930
 
924
931
    def render(self, opf_stream=sys.stdout, ncx_stream=None,
925
 
               ncx_manifest_entry=None):
 
932
               ncx_manifest_entry=None, encoding=None):
926
933
        from calibre.resources import opf_template
927
934
        from calibre.utils.genshi.template import MarkupTemplate
 
935
        if encoding is None:
 
936
            encoding = 'utf-8'
928
937
        template = MarkupTemplate(opf_template)
 
938
        toc = getattr(self, 'toc', None)
929
939
        if self.manifest:
930
940
            self.manifest.set_basedir(self.base_path)
931
 
            if ncx_manifest_entry is not None:
 
941
            if ncx_manifest_entry is not None and toc is not None:
932
942
                if not os.path.isabs(ncx_manifest_entry):
933
943
                    ncx_manifest_entry = os.path.join(self.base_path, ncx_manifest_entry)
934
944
                remove = [i for i in self.manifest if i.id == 'ncx']
945
955
                cover = os.path.abspath(os.path.join(self.base_path, cover))
946
956
            self.guide.set_cover(cover)
947
957
        self.guide.set_basedir(self.base_path)
948
 
        opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
 
958
        opf = template.generate(
 
959
                __appname__=__appname__, mi=self,
 
960
                __version__=__version__).render('xml', encoding=encoding)
 
961
        opf_stream.write('<?xml version="1.0" encoding="%s" ?>\n'
 
962
                %encoding.upper())
949
963
        opf_stream.write(opf)
950
964
        opf_stream.flush()
951
 
        toc = getattr(self, 'toc', None)
952
965
        if toc is not None and ncx_stream is not None:
953
966
            toc.render(ncx_stream, self.application_id)
954
967
            ncx_stream.flush()
955
968
 
956
969
 
 
970
def metadata_to_opf(mi, as_string=True):
 
971
    from lxml import etree
 
972
    import textwrap
 
973
    from calibre.ebooks.oeb.base import OPF, DC
 
974
 
 
975
    if not mi.application_id:
 
976
        mi.application_id = str(uuid.uuid4())
 
977
 
 
978
    if not mi.book_producer:
 
979
        mi.book_producer = __appname__ + ' (%s) '%__version__ + \
 
980
            '[http://calibre-ebook.com]'
 
981
 
 
982
    if not mi.language:
 
983
        mi.language = 'UND'
 
984
 
 
985
    root = etree.fromstring(textwrap.dedent(
 
986
    '''
 
987
    <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="%(a)s_id">
 
988
        <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
 
989
            <dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
 
990
        </metadata>
 
991
        <guide/>
 
992
    </package>
 
993
    '''%dict(a=__appname__, id=mi.application_id)))
 
994
    metadata = root[0]
 
995
    guide = root[1]
 
996
    metadata[0].tail = '\n'+(' '*8)
 
997
    def factory(tag, text=None, sort=None, role=None, scheme=None, name=None,
 
998
            content=None):
 
999
        attrib = {}
 
1000
        if sort:
 
1001
            attrib[OPF('file-as')] = sort
 
1002
        if role:
 
1003
            attrib[OPF('role')] = role
 
1004
        if scheme:
 
1005
            attrib[OPF('scheme')] = scheme
 
1006
        if name:
 
1007
            attrib['name'] = name
 
1008
        if content:
 
1009
            attrib['content'] = content
 
1010
        elem = metadata.makeelement(tag, attrib=attrib)
 
1011
        elem.tail = '\n'+(' '*8)
 
1012
        if text:
 
1013
            elem.text = text.strip()
 
1014
        metadata.append(elem)
 
1015
 
 
1016
    factory(DC('title'), mi.title, mi.title_sort)
 
1017
    for au in mi.authors:
 
1018
        factory(DC('creator'), au, mi.author_sort, 'aut')
 
1019
    factory(DC('contributor'), mi.book_producer, __appname__, 'bkp')
 
1020
    if hasattr(mi.pubdate, 'isoformat'):
 
1021
        factory(DC('date'), mi.pubdate.isoformat())
 
1022
    factory(DC('language'), mi.language)
 
1023
    if mi.category:
 
1024
        factory(DC('type'), mi.category)
 
1025
    if mi.comments:
 
1026
        factory(DC('description'), mi.comments)
 
1027
    if mi.publisher:
 
1028
        factory(DC('publisher'), mi.publisher)
 
1029
    if mi.isbn:
 
1030
        factory(DC('identifier'), mi.isbn, scheme='ISBN')
 
1031
    if mi.rights:
 
1032
        factory(DC('rights'), mi.rights)
 
1033
    if mi.tags:
 
1034
        for tag in mi.tags:
 
1035
            factory(DC('subject'), tag)
 
1036
    meta = lambda n, c: factory('meta', name='calibre:'+n, content=c)
 
1037
    if mi.series:
 
1038
        meta('series', mi.series)
 
1039
    if mi.series_index is not None:
 
1040
        meta('series_index', mi.format_series_index())
 
1041
    if mi.rating is not None:
 
1042
        meta('rating', str(mi.rating))
 
1043
    if hasattr(mi.timestamp, 'isoformat'):
 
1044
        meta('timestamp', mi.timestamp.isoformat())
 
1045
    if mi.publication_type:
 
1046
        meta('publication_type', mi.publication_type)
 
1047
 
 
1048
    metadata[-1].tail = '\n' +(' '*4)
 
1049
 
 
1050
    if mi.cover:
 
1051
        if not isinstance(mi.cover, unicode):
 
1052
            mi.cover = mi.cover.decode(filesystem_encoding)
 
1053
        guide.text = '\n'+(' '*8)
 
1054
        r = guide.makeelement(OPF('reference'),
 
1055
                attrib={'type':'cover', 'title':_('Cover'), 'href':mi.cover})
 
1056
        r.tail = '\n' +(' '*4)
 
1057
        guide.append(r)
 
1058
    return etree.tostring(root, pretty_print=True, encoding='utf-8',
 
1059
            xml_declaration=True) if as_string else root
 
1060
 
 
1061
 
 
1062
def test_m2o():
 
1063
    from datetime import datetime
 
1064
    from cStringIO import StringIO
 
1065
    mi = MetaInformation('test & title', ['a"1', "a'2"])
 
1066
    mi.title_sort = 'a\'"b'
 
1067
    mi.author_sort = 'author sort'
 
1068
    mi.pubdate = datetime.now()
 
1069
    mi.language = 'en'
 
1070
    mi.category = 'test'
 
1071
    mi.comments = 'what a fun book\n\n'
 
1072
    mi.publisher = 'publisher'
 
1073
    mi.isbn = 'boooo'
 
1074
    mi.tags = ['a', 'b']
 
1075
    mi.series = 's"c\'l&<>'
 
1076
    mi.series_index = 3.34
 
1077
    mi.rating = 3
 
1078
    mi.timestamp = datetime.now()
 
1079
    mi.publication_type = 'ooooo'
 
1080
    mi.rights = 'yes'
 
1081
    mi.cover = 'asd.jpg'
 
1082
    opf = metadata_to_opf(mi)
 
1083
    print opf
 
1084
    newmi = MetaInformation(OPF(StringIO(opf)))
 
1085
    for attr in ('author_sort', 'title_sort', 'comments', 'category',
 
1086
                    'publisher', 'series', 'series_index', 'rating',
 
1087
                    'isbn', 'tags', 'cover_data', 'application_id',
 
1088
                    'language', 'cover',
 
1089
                    'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
 
1090
                    'pubdate', 'rights', 'publication_type'):
 
1091
        o, n = getattr(mi, attr), getattr(newmi, attr)
 
1092
        if o != n and o.strip() != n.strip():
 
1093
            print 'FAILED:', attr, getattr(mi, attr), '!=', getattr(newmi, attr)
 
1094
 
 
1095
 
957
1096
class OPFTest(unittest.TestCase):
958
1097
 
959
1098
    def setUp(self):
1018
1157
 
1019
1158
def test():
1020
1159
    unittest.TextTestRunner(verbosity=2).run(suite())
1021
 
 
1022
 
 
1023
 
def option_parser():
1024
 
    from calibre.ebooks.metadata import get_parser
1025
 
    parser = get_parser('opf')
1026
 
    parser.add_option('--language', default=None, help=_('Set the dc:language field'))
1027
 
    return parser
1028
 
 
1029
 
def main(args=sys.argv):
1030
 
    parser = option_parser()
1031
 
    opts, args = parser.parse_args(args)
1032
 
    if len(args) != 2:
1033
 
        parser.print_help()
1034
 
        return 1
1035
 
    opfpath = os.path.abspath(args[1])
1036
 
    basedir = os.path.dirname(opfpath)
1037
 
    mi = MetaInformation(OPF(open(opfpath, 'rb'), basedir))
1038
 
    write = False
1039
 
    if opts.title is not None:
1040
 
        mi.title = opts.title
1041
 
        write = True
1042
 
    if opts.authors is not None:
1043
 
        aus = [i.strip() for i in opts.authors.split(',')]
1044
 
        mi.authors = aus
1045
 
        write = True
1046
 
    if opts.category is not None:
1047
 
        mi.category = opts.category
1048
 
        write = True
1049
 
    if opts.comment is not None:
1050
 
        mi.comments = opts.comment
1051
 
        write = True
1052
 
    if opts.language is not None:
1053
 
        mi.language = opts.language
1054
 
        write = True
1055
 
    if write:
1056
 
        mo = OPFCreator(basedir, mi)
1057
 
        ncx = cStringIO.StringIO()
1058
 
        mo.render(open(args[1], 'wb'), ncx)
1059
 
        ncx = ncx.getvalue()
1060
 
        if ncx:
1061
 
            f = glob.glob(os.path.join(os.path.dirname(args[1]), '*.ncx'))
1062
 
            if f:
1063
 
                f = open(f[0], 'wb')
1064
 
            else:
1065
 
                f = open(os.path.splitext(args[1])[0]+'.ncx', 'wb')
1066
 
            f.write(ncx)
1067
 
            f.close()
1068
 
    print MetaInformation(OPF(open(opfpath, 'rb'), basedir))
1069
 
    return 0
1070
 
 
1071
 
 
1072
 
 
1073
 
if __name__ == '__main__':
1074
 
    sys.exit(main())