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

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/metadata/opf.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:
2
2
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
3
3
'''Read/Write metadata from Open Packaging Format (.opf) files.'''
4
4
 
5
 
import sys, re, os, glob
6
 
import cStringIO
 
5
import re, os
7
6
import uuid
8
7
from urllib import unquote, quote
9
8
 
10
9
from calibre.constants import __appname__, __version__
11
 
from calibre.ebooks.metadata import MetaInformation
 
10
from calibre.ebooks.metadata import MetaInformation, string_to_authors
12
11
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
13
12
from calibre.ebooks.lrf import entity_to_unicode
14
 
from calibre.ebooks.metadata import get_parser, Resource, ResourceCollection
 
13
from calibre.ebooks.metadata import Resource, ResourceCollection
15
14
from calibre.ebooks.metadata.toc import TOC
16
15
 
17
16
class OPFSoup(BeautifulStoneSoup):
18
 
    
 
17
 
19
18
    def __init__(self, raw):
20
 
        BeautifulStoneSoup.__init__(self, raw,  
 
19
        BeautifulStoneSoup.__init__(self, raw,
21
20
                                  convertEntities=BeautifulSoup.HTML_ENTITIES,
22
21
                                  selfClosingTags=['item', 'itemref', 'reference'])
23
22
 
24
23
class ManifestItem(Resource):
25
 
    
 
24
 
26
25
    @staticmethod
27
26
    def from_opf_manifest_item(item, basedir):
28
27
        if item.has_key('href'):
37
36
            if mt:
38
37
                res.mime_type = mt
39
38
            return res
40
 
    
41
 
    @apply
42
 
    def media_type():
 
39
 
 
40
    @dynamic_property
 
41
    def media_type(self):
43
42
        def fget(self):
44
43
            return self.mime_type
45
44
        def fset(self, val):
46
45
            self.mime_type = val
47
46
        return property(fget=fget, fset=fset)
48
 
    
49
 
        
 
47
 
 
48
 
50
49
    def __unicode__(self):
51
50
        return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type)
52
 
    
 
51
 
53
52
    def __str__(self):
54
53
        return unicode(self).encode('utf-8')
55
 
    
 
54
 
56
55
    def __repr__(self):
57
56
        return unicode(self)
58
 
        
59
 
    
 
57
 
 
58
 
60
59
    def __getitem__(self, index):
61
60
        if index == 0:
62
61
            return self.href()
63
62
        if index == 1:
64
63
            return self.media_type
65
64
        raise IndexError('%d out of bounds.'%index)
66
 
        
 
65
 
67
66
 
68
67
class Manifest(ResourceCollection):
69
 
    
 
68
 
70
69
    @staticmethod
71
70
    def from_opf_manifest_element(manifest, dir):
72
71
        m = Manifest()
81
80
            except ValueError:
82
81
                continue
83
82
        return m
84
 
    
 
83
 
85
84
    @staticmethod
86
85
    def from_paths(entries):
87
86
        '''
96
95
            m.next_id += 1
97
96
            m.append(mi)
98
97
        return m
99
 
    
 
98
 
100
99
    def __init__(self):
101
100
        ResourceCollection.__init__(self)
102
101
        self.next_id = 1
103
 
            
104
 
                
 
102
 
 
103
 
105
104
    def item(self, id):
106
105
        for i in self:
107
106
            if i.id == id:
108
107
                return i
109
 
            
 
108
 
110
109
    def id_for_path(self, path):
111
110
        path = os.path.normpath(os.path.abspath(path))
112
111
        for i in self:
113
112
            if i.path and os.path.normpath(i.path) == path:
114
 
                return i.id    
115
 
            
 
113
                return i.id
 
114
 
116
115
    def path_for_id(self, id):
117
116
        for i in self:
118
117
            if i.id == id:
119
118
                return i.path
120
119
 
121
120
class Spine(ResourceCollection):
122
 
    
 
121
 
123
122
    class Item(Resource):
124
 
        
 
123
 
125
124
        def __init__(self, idfunc, *args, **kwargs):
126
125
            Resource.__init__(self, *args, **kwargs)
127
126
            self.is_linear = True
128
127
            self.id = idfunc(self.path)
129
 
        
 
128
 
130
129
    @staticmethod
131
130
    def from_opf_spine_element(spine, manifest):
132
131
        s = Spine(manifest)
137
136
                r.is_linear = itemref.get('linear', 'yes') == 'yes'
138
137
                s.append(r)
139
138
        return s
140
 
                
 
139
 
141
140
    @staticmethod
142
141
    def from_paths(paths, manifest):
143
142
        s = Spine(manifest)
147
146
            except:
148
147
                continue
149
148
        return s
150
 
            
151
 
            
152
 
    
 
149
 
 
150
 
 
151
 
153
152
    def __init__(self, manifest):
154
153
        ResourceCollection.__init__(self)
155
154
        self.manifest = manifest
156
 
            
157
 
                    
 
155
 
 
156
 
158
157
    def linear_items(self):
159
158
        for r in self:
160
159
            if r.is_linear:
164
163
        for r in self:
165
164
            if not r.is_linear:
166
165
                yield r.path
167
 
        
 
166
 
168
167
    def items(self):
169
168
        for i in self:
170
169
            yield i.path
171
 
    
172
 
            
 
170
 
 
171
 
173
172
class Guide(ResourceCollection):
174
 
    
 
173
 
175
174
    class Reference(Resource):
176
 
        
 
175
 
177
176
        @staticmethod
178
177
        def from_opf_resource_item(ref, basedir):
179
178
            title, href, type = ref.get('title', ''), ref['href'], ref['type']
181
180
            res.title = title
182
181
            res.type = type
183
182
            return res
184
 
        
 
183
 
185
184
        def __repr__(self):
186
185
            ans = '<reference type="%s" href="%s" '%(self.type, self.href())
187
186
            if self.title:
188
187
                ans += 'title="%s" '%self.title
189
188
            return ans + '/>'
190
 
        
191
 
        
 
189
 
 
190
 
192
191
    @staticmethod
193
192
    def from_opf_guide(guide_elem, base_dir=os.getcwdu()):
194
193
        coll = Guide()
199
198
            except:
200
199
                continue
201
200
        return coll
202
 
        
 
201
 
203
202
    def set_cover(self, path):
204
203
        map(self.remove, [i for i in self if 'cover' in i.type.lower()])
205
204
        for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
206
205
            self.append(Guide.Reference(path, is_path=True))
207
206
            self[-1].type = type
208
207
            self[-1].title = ''
209
 
        
 
208
 
210
209
 
211
210
class standard_field(object):
212
 
    
 
211
 
213
212
    def __init__(self, name):
214
213
        self.name = name
215
 
        
 
214
 
216
215
    def __get__(self, obj, typ=None):
217
216
        return getattr(obj, 'get_'+self.name)()
218
 
    
 
217
 
219
218
 
220
219
class OPF(MetaInformation):
221
 
    
 
220
 
222
221
    MIMETYPE = 'application/oebps-package+xml'
223
222
    ENTITY_PATTERN = re.compile(r'&(\S+?);')
224
 
    
 
223
 
225
224
    uid            = standard_field('uid')
226
225
    application_id = standard_field('application_id')
227
226
    title          = standard_field('title')
238
237
    series_index   = standard_field('series_index')
239
238
    rating         = standard_field('rating')
240
239
    tags           = standard_field('tags')
241
 
    
 
240
 
242
241
    def __init__(self):
243
242
        raise NotImplementedError('Abstract base class')
244
 
    
245
 
    @apply
246
 
    def package():
 
243
 
 
244
    @dynamic_property
 
245
    def package(self):
247
246
        def fget(self):
248
247
            return self.soup.find(re.compile('package'))
249
248
        return property(fget=fget)
250
 
    
251
 
    @apply
252
 
    def metadata():
 
249
 
 
250
    @dynamic_property
 
251
    def metadata(self):
253
252
        def fget(self):
254
253
            return self.package.find(re.compile('metadata'))
255
254
        return property(fget=fget)
256
 
    
257
 
    
 
255
 
 
256
 
258
257
    def get_title(self):
259
258
        title = self.metadata.find('dc:title')
260
259
        if title and title.string:
261
260
            return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
262
261
        return self.default_title.strip()
263
 
    
 
262
 
264
263
    def get_authors(self):
265
264
        creators = self.metadata.findAll('dc:creator')
266
265
        for elem in creators:
271
270
                role = 'aut'
272
271
            if role == 'aut' and elem.string:
273
272
                raw = self.ENTITY_PATTERN.sub(entity_to_unicode, elem.string)
274
 
                au = raw.split(',')
275
 
                ans = []
276
 
                for i in au:
277
 
                    ans.extend(i.split('&'))
278
 
                return [a.strip() for a in ans]
 
273
                return string_to_authors(raw)
279
274
        return []
280
 
    
 
275
 
281
276
    def get_author_sort(self):
282
277
        creators = self.metadata.findAll('dc:creator')
283
278
        for elem in creators:
288
283
                fa = elem.get('file-as')
289
284
                return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None
290
285
        return None
291
 
    
 
286
 
292
287
    def get_title_sort(self):
293
288
        title = self.package.find('dc:title')
294
289
        if title:
295
290
            if title.has_key('file-as'):
296
291
                return title['file-as'].strip()
297
292
        return None
298
 
    
 
293
 
299
294
    def get_comments(self):
300
295
        comments = self.soup.find('dc:description')
301
296
        if comments and comments.string:
302
297
            return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip()
303
298
        return None
304
 
    
 
299
 
305
300
    def get_uid(self):
306
301
        package = self.package
307
302
        if package.has_key('unique-identifier'):
308
303
            return package['unique-identifier']
309
 
        
 
304
 
310
305
    def get_category(self):
311
306
        category = self.soup.find('dc:type')
312
307
        if category and category.string:
313
308
            return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
314
309
        return None
315
 
    
 
310
 
316
311
    def get_publisher(self):
317
312
        publisher = self.soup.find('dc:publisher')
318
313
        if publisher and publisher.string:
319
314
            return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
320
315
        return None
321
 
    
 
316
 
322
317
    def get_isbn(self):
323
318
        for item in self.metadata.findAll('dc:identifier'):
324
319
            scheme = item.get('scheme')
327
322
            if scheme is not None and scheme.lower() == 'isbn' and item.string:
328
323
                return str(item.string).strip()
329
324
        return None
330
 
    
 
325
 
331
326
    def get_language(self):
332
327
        item = self.metadata.find('dc:language')
333
328
        if not item:
334
329
            return _('Unknown')
335
330
        return ''.join(item.findAll(text=True)).strip()
336
 
    
 
331
 
337
332
    def get_application_id(self):
338
333
        for item in self.metadata.findAll('dc:identifier'):
339
334
            scheme = item.get('scheme', None)
342
337
            if scheme in ['libprs500', 'calibre']:
343
338
                return str(item.string).strip()
344
339
        return None
345
 
    
 
340
 
346
341
    def get_cover(self):
347
342
        guide = getattr(self, 'guide', [])
348
343
        if not guide:
352
347
            matches = [r for r in references if r.type.lower() == candidate and r.path]
353
348
            if matches:
354
349
                return matches[0].path
355
 
    
 
350
 
356
351
    def possible_cover_prefixes(self):
357
352
        isbn, ans = [], []
358
353
        for item in self.metadata.findAll('dc:identifier'):
363
358
        for item in isbn:
364
359
            ans.append(item[1].replace('-', ''))
365
360
        return ans
366
 
    
 
361
 
367
362
    def get_series(self):
368
363
        s = self.metadata.find('series')
369
364
        if s is not None:
370
365
            return str(s.string).strip()
371
366
        return None
372
 
    
 
367
 
373
368
    def get_series_index(self):
374
369
        s = self.metadata.find('series-index')
375
370
        if s and s.string:
376
371
            try:
377
 
                return int(str(s.string).strip())
 
372
                return float(str(s.string).strip())
378
373
            except:
379
374
                return None
380
375
        return None
381
 
    
 
376
 
382
377
    def get_rating(self):
383
378
        s = self.metadata.find('rating')
384
379
        if s and s.string:
387
382
            except:
388
383
                return None
389
384
        return None
390
 
    
 
385
 
391
386
    def get_tags(self):
392
387
        ans = []
393
388
        subs = self.soup.findAll('dc:subject')
396
391
            if val:
397
392
                ans.append(val)
398
393
        return [unicode(a).strip() for a in ans]
399
 
    
400
 
    
 
394
 
 
395
 
401
396
class OPFReader(OPF):
402
 
    
 
397
 
403
398
    def __init__(self, stream, dir=os.getcwdu()):
404
399
        manage = False
405
400
        if not hasattr(stream, 'read'):
406
401
            manage = True
407
402
            dir = os.path.dirname(stream)
408
403
            stream = open(stream, 'rb')
409
 
        self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown' 
 
404
        self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown'
410
405
        if hasattr(stream, 'seek'):
411
406
            stream.seek(0)
412
407
        self.soup = OPFSoup(stream.read())
420
415
        spine = self.soup.find(re.compile('spine'))
421
416
        if spine is not None:
422
417
            self.spine = Spine.from_opf_spine_element(spine, self.manifest)
423
 
        
 
418
 
424
419
        self.toc = TOC(base_path=dir)
425
420
        self.toc.read_from_opf(self)
426
421
        guide = self.soup.find(re.compile('guide'))
427
422
        if guide is not None:
428
423
            self.guide = Guide.from_opf_guide(guide, dir)
429
 
        self.base_dir = dir 
 
424
        self.base_dir = dir
430
425
        self.cover_data = (None, None)
431
 
        
432
 
        
 
426
 
 
427
 
433
428
class OPFCreator(MetaInformation):
434
 
    
 
429
 
435
430
    def __init__(self, base_path, *args, **kwargs):
436
431
        '''
437
432
        Initialize.
451
446
            self.guide = Guide()
452
447
        if self.cover:
453
448
            self.guide.set_cover(self.cover)
454
 
        
455
 
        
 
449
 
 
450
 
456
451
    def create_manifest(self, entries):
457
452
        '''
458
453
        Create <manifest>
459
 
        
 
454
 
460
455
        `entries`: List of (path, mime-type) If mime-type is None it is autodetected
461
456
        '''
462
 
        entries = map(lambda x: x if os.path.isabs(x[0]) else 
 
457
        entries = map(lambda x: x if os.path.isabs(x[0]) else
463
458
                      (os.path.abspath(os.path.join(self.base_path, x[0])), x[1]),
464
459
                      entries)
465
460
        self.manifest = Manifest.from_paths(entries)
466
461
        self.manifest.set_basedir(self.base_path)
467
 
        
 
462
 
468
463
    def create_manifest_from_files_in(self, files_and_dirs):
469
464
        entries = []
470
 
        
 
465
 
471
466
        def dodir(dir):
472
467
            for spec in os.walk(dir):
473
468
                root, files = spec[0], spec[-1]
474
469
                for name in files:
475
470
                    path = os.path.join(root, name)
476
471
                    if os.path.isfile(path):
477
 
                        entries.append((path, None)) 
478
 
        
 
472
                        entries.append((path, None))
 
473
 
479
474
        for i in files_and_dirs:
480
475
            if os.path.isdir(i):
481
476
                dodir(i)
482
477
            else:
483
478
                entries.append((i, None))
484
 
                
485
 
        self.create_manifest(entries)    
486
 
            
 
479
 
 
480
        self.create_manifest(entries)
 
481
 
487
482
    def create_spine(self, entries):
488
483
        '''
489
484
        Create the <spine> element. Must first call :method:`create_manifest`.
490
 
        
 
485
 
491
486
        `entries`: List of paths
492
487
        '''
493
 
        entries = map(lambda x: x if os.path.isabs(x) else 
 
488
        entries = map(lambda x: x if os.path.isabs(x) else
494
489
                      os.path.abspath(os.path.join(self.base_path, x)), entries)
495
490
        self.spine = Spine.from_paths(entries, self.manifest)
496
 
        
 
491
 
497
492
    def set_toc(self, toc):
498
493
        '''
499
494
        Set the toc. You must call :method:`create_spine` before calling this
500
495
        method.
501
 
        
 
496
 
502
497
        :param toc: A :class:`TOC` object
503
498
        '''
504
499
        self.toc = toc
505
 
        
 
500
 
506
501
    def create_guide(self, guide_element):
507
502
        self.guide = Guide.from_opf_guide(guide_element, self.base_path)
508
503
        self.guide.set_basedir(self.base_path)
509
 
            
 
504
 
510
505
    def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
511
506
        from calibre.resources import opf_template
512
507
        from calibre.utils.genshi.template import MarkupTemplate
530
525
                cover = os.path.abspath(os.path.join(self.base_path, cover))
531
526
            self.guide.set_cover(cover)
532
527
        self.guide.set_basedir(self.base_path)
533
 
        
 
528
 
534
529
        opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
535
530
        if not opf.startswith('<?xml '):
536
531
            opf = '<?xml version="1.0"  encoding="UTF-8"?>\n'+opf
540
535
        if toc is not None and ncx_stream is not None:
541
536
            toc.render(ncx_stream, self.application_id)
542
537
            ncx_stream.flush()
543
 
    
544
 
def option_parser():
545
 
    return get_parser('opf')
546
 
 
547
 
def main(args=sys.argv):
548
 
    parser = option_parser()
549
 
    opts, args = parser.parse_args(args)
550
 
    if len(args) != 2:
551
 
        parser.print_help()
552
 
        return 1
553
 
    mi = MetaInformation(OPFReader(open(args[1], 'rb'), os.path.abspath(os.path.dirname(args[1]))))
554
 
    write = False
555
 
    if opts.title is not None:
556
 
        mi.title = opts.title.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
557
 
        write = True
558
 
    if opts.authors is not None:
559
 
        aus = [i.strip().replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') for i in opts.authors.split(',')]
560
 
        mi.authors = aus
561
 
        write = True
562
 
    if opts.category is not None:
563
 
        mi.category = opts.category.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
564
 
        write = True
565
 
    if opts.comment is not None:
566
 
        mi.comments = opts.comment.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
567
 
        write = True
568
 
    if write:
569
 
        mo = OPFCreator(os.path.dirname(args[1]), mi)
570
 
        ncx = cStringIO.StringIO()
571
 
        mo.render(open(args[1], 'wb'), ncx)
572
 
        ncx = ncx.getvalue()
573
 
        if ncx:
574
 
            f = glob.glob(os.path.join(os.path.dirname(args[1]), '*.ncx'))
575
 
            if f:
576
 
                f = open(f[0], 'wb')
577
 
            else:
578
 
                f = open(os.path.splitext(args[1])[0]+'.ncx', 'wb')
579
 
            f.write(ncx)
580
 
            f.close()
581
 
    print MetaInformation(OPFReader(open(args[1], 'rb'), os.path.abspath(os.path.dirname(args[1]))))
582
 
    return 0
583
 
 
584
 
if __name__ == '__main__':
585
 
    sys.exit(main())
 
538