1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
3
4
5__license__   = 'GPL v3'
6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
7__docformat__ = 'restructuredtext en'
8
9import copy, traceback
10
11from calibre import prints
12from calibre.constants import DEBUG
13from calibre.ebooks.metadata.book import (SC_COPYABLE_FIELDS,
14        SC_FIELDS_COPY_NOT_NULL, STANDARD_METADATA_FIELDS,
15        TOP_LEVEL_IDENTIFIERS, ALL_METADATA_FIELDS)
16from calibre.library.field_metadata import FieldMetadata
17from calibre.utils.icu import sort_key
18from polyglot.builtins import iteritems, string_or_bytes
19
20# Special sets used to optimize the performance of getting and setting
21# attributes on Metadata objects
22SIMPLE_GET = frozenset(STANDARD_METADATA_FIELDS - TOP_LEVEL_IDENTIFIERS)
23SIMPLE_SET = frozenset(SIMPLE_GET - {'identifiers'})
24
25
26def human_readable(size, precision=2):
27    """ Convert a size in bytes into megabytes """
28    ans = size/(1024*1024)
29    if ans < 0.1:
30        return '<0.1MB'
31    return ('%.'+str(precision)+'f'+ 'MB') % ans
32
33
34NULL_VALUES = {
35                'user_metadata': {},
36                'cover_data'   : (None, None),
37                'tags'         : [],
38                'identifiers'  : {},
39                'languages'    : [],
40                'device_collections': [],
41                'author_sort_map': {},
42                'authors'      : [_('Unknown')],
43                'author_sort'  : _('Unknown'),
44                'title'        : _('Unknown'),
45                'user_categories' : {},
46                'author_link_map' : {},
47                'language'     : 'und'
48}
49
50field_metadata = FieldMetadata()
51
52
53def reset_field_metadata():
54    global field_metadata
55    field_metadata = FieldMetadata()
56
57
58ck = lambda typ: icu_lower(typ).strip().replace(':', '').replace(',', '')
59cv = lambda val: val.strip().replace(',', '|')
60
61
62class Metadata:
63
64    '''
65    A class representing all the metadata for a book. The various standard metadata
66    fields are available as attributes of this object. You can also stick
67    arbitrary attributes onto this object.
68
69    Metadata from custom columns should be accessed via the get() method,
70    passing in the lookup name for the column, for example: "#mytags".
71
72    Use the :meth:`is_null` method to test if a field is null.
73
74    This object also has functions to format fields into strings.
75
76    The list of standard metadata fields grows with time is in
77    :data:`STANDARD_METADATA_FIELDS`.
78
79    Please keep the method based API of this class to a minimum. Every method
80    becomes a reserved field name.
81    '''
82    __calibre_serializable__ = True
83
84    def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None,
85                 formatter=None):
86        '''
87        @param title: title or ``_('Unknown')``
88        @param authors: List of strings or []
89        @param other: None or a metadata object
90        '''
91        _data = copy.deepcopy(NULL_VALUES)
92        _data.pop('language')
93        object.__setattr__(self, '_data', _data)
94        if other is not None:
95            self.smart_update(other)
96        else:
97            if title:
98                self.title = title
99            if authors:
100                # List of strings or []
101                self.author = list(authors) if authors else []  # Needed for backward compatibility
102                self.authors = list(authors) if authors else []
103        from calibre.ebooks.metadata.book.formatter import SafeFormat
104        self.formatter = SafeFormat() if formatter is None else formatter
105        self.template_cache = template_cache
106
107    def is_null(self, field):
108        '''
109        Return True if the value of field is null in this object.
110        'null' means it is unknown or evaluates to False. So a title of
111        _('Unknown') is null or a language of 'und' is null.
112
113        Be careful with numeric fields since this will return True for zero as
114        well as None.
115
116        Also returns True if the field does not exist.
117        '''
118        try:
119            null_val = NULL_VALUES.get(field, None)
120            val = getattr(self, field, None)
121            return not val or val == null_val
122        except:
123            return True
124
125    def set_null(self, field):
126        null_val = copy.copy(NULL_VALUES.get(field))
127        setattr(self, field, null_val)
128
129    def __getattribute__(self, field):
130        _data = object.__getattribute__(self, '_data')
131        if field in SIMPLE_GET:
132            return _data.get(field, None)
133        if field in TOP_LEVEL_IDENTIFIERS:
134            return _data.get('identifiers').get(field, None)
135        if field == 'language':
136            try:
137                return _data.get('languages', [])[0]
138            except:
139                return NULL_VALUES['language']
140        try:
141            return object.__getattribute__(self, field)
142        except AttributeError:
143            pass
144        if field in _data['user_metadata']:
145            d = _data['user_metadata'][field]
146            val = d['#value#']
147            if d['datatype'] != 'composite':
148                return val
149            if val is None:
150                d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field
151                val = d['#value#'] = self.formatter.safe_format(
152                                            d['display']['composite_template'],
153                                            self,
154                                            _('TEMPLATE ERROR'),
155                                            self, column_name=field,
156                                            template_cache=self.template_cache).strip()
157            return val
158        if field.startswith('#') and field.endswith('_index'):
159            try:
160                return self.get_extra(field[:-6])
161            except:
162                pass
163        raise AttributeError(
164                'Metadata object has no attribute named: '+ repr(field))
165
166    def __setattr__(self, field, val, extra=None):
167        _data = object.__getattribute__(self, '_data')
168        if field in SIMPLE_SET:
169            if val is None:
170                val = copy.copy(NULL_VALUES.get(field, None))
171            _data[field] = val
172        elif field in TOP_LEVEL_IDENTIFIERS:
173            field, val = self._clean_identifier(field, val)
174            identifiers = _data['identifiers']
175            identifiers.pop(field, None)
176            if val:
177                identifiers[field] = val
178        elif field == 'identifiers':
179            if not val:
180                val = copy.copy(NULL_VALUES.get('identifiers', None))
181            self.set_identifiers(val)
182        elif field == 'language':
183            langs = []
184            if val and val.lower() != 'und':
185                langs = [val]
186            _data['languages'] = langs
187        elif field in _data['user_metadata']:
188            _data['user_metadata'][field]['#value#'] = val
189            _data['user_metadata'][field]['#extra#'] = extra
190        else:
191            # You are allowed to stick arbitrary attributes onto this object as
192            # long as they don't conflict with global or user metadata names
193            # Don't abuse this privilege
194            self.__dict__[field] = val
195
196    def __iter__(self):
197        return iter(object.__getattribute__(self, '_data'))
198
199    def has_key(self, key):
200        return key in object.__getattribute__(self, '_data')
201
202    def deepcopy(self, class_generator=lambda : Metadata(None)):
203        ''' Do not use this method unless you know what you are doing, if you
204        want to create a simple clone of this object, use :meth:`deepcopy_metadata`
205        instead. Class_generator must be a function that returns an instance
206        of Metadata or a subclass of it.'''
207        m = class_generator()
208        if not isinstance(m, Metadata):
209            return None
210        object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__))
211        return m
212
213    def deepcopy_metadata(self):
214        m = Metadata(None)
215        object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
216        return m
217
218    def get(self, field, default=None):
219        try:
220            return self.__getattribute__(field)
221        except AttributeError:
222            return default
223
224    def get_extra(self, field, default=None):
225        _data = object.__getattribute__(self, '_data')
226        if field in _data['user_metadata']:
227            try:
228                return _data['user_metadata'][field]['#extra#']
229            except:
230                return default
231        raise AttributeError(
232                'Metadata object has no attribute named: '+ repr(field))
233
234    def set(self, field, val, extra=None):
235        self.__setattr__(field, val, extra)
236
237    def get_identifiers(self):
238        '''
239        Return a copy of the identifiers dictionary.
240        The dict is small, and the penalty for using a reference where a copy is
241        needed is large. Also, we don't want any manipulations of the returned
242        dict to show up in the book.
243        '''
244        ans = object.__getattribute__(self,
245            '_data')['identifiers']
246        if not ans:
247            ans = {}
248        return copy.deepcopy(ans)
249
250    def _clean_identifier(self, typ, val):
251        if typ:
252            typ = ck(typ)
253        if val:
254            val = cv(val)
255        return typ, val
256
257    def set_identifiers(self, identifiers):
258        '''
259        Set all identifiers. Note that if you previously set ISBN, calling
260        this method will delete it.
261        '''
262        cleaned = {ck(k):cv(v) for k, v in iteritems(identifiers) if k and v}
263        object.__getattribute__(self, '_data')['identifiers'] = cleaned
264
265    def set_identifier(self, typ, val):
266        'If val is empty, deletes identifier of type typ'
267        typ, val = self._clean_identifier(typ, val)
268        if not typ:
269            return
270        identifiers = object.__getattribute__(self,
271            '_data')['identifiers']
272
273        identifiers.pop(typ, None)
274        if val:
275            identifiers[typ] = val
276
277    def has_identifier(self, typ):
278        identifiers = object.__getattribute__(self,
279            '_data')['identifiers']
280        return typ in identifiers
281
282    # field-oriented interface. Intended to be the same as in LibraryDatabase
283
284    def standard_field_keys(self):
285        '''
286        return a list of all possible keys, even if this book doesn't have them
287        '''
288        return STANDARD_METADATA_FIELDS
289
290    def custom_field_keys(self):
291        '''
292        return a list of the custom fields in this book
293        '''
294        return iter(object.__getattribute__(self, '_data')['user_metadata'])
295
296    def all_field_keys(self):
297        '''
298        All field keys known by this instance, even if their value is None
299        '''
300        _data = object.__getattribute__(self, '_data')
301        return frozenset(ALL_METADATA_FIELDS.union(frozenset(_data['user_metadata'])))
302
303    def metadata_for_field(self, key):
304        '''
305        return metadata describing a standard or custom field.
306        '''
307        if key not in self.custom_field_keys():
308            return self.get_standard_metadata(key, make_copy=False)
309        return self.get_user_metadata(key, make_copy=False)
310
311    def all_non_none_fields(self):
312        '''
313        Return a dictionary containing all non-None metadata fields, including
314        the custom ones.
315        '''
316        result = {}
317        _data = object.__getattribute__(self, '_data')
318        for attr in STANDARD_METADATA_FIELDS:
319            v = _data.get(attr, None)
320            if v is not None:
321                result[attr] = v
322        # separate these because it uses the self.get(), not _data.get()
323        for attr in TOP_LEVEL_IDENTIFIERS:
324            v = self.get(attr, None)
325            if v is not None:
326                result[attr] = v
327        for attr in _data['user_metadata']:
328            v = self.get(attr, None)
329            if v is not None:
330                result[attr] = v
331                if _data['user_metadata'][attr]['datatype'] == 'series':
332                    result[attr+'_index'] = _data['user_metadata'][attr]['#extra#']
333        return result
334
335    # End of field-oriented interface
336
337    # Extended interfaces. These permit one to get copies of metadata dictionaries, and to
338    # get and set custom field metadata
339
340    def get_standard_metadata(self, field, make_copy):
341        '''
342        return field metadata from the field if it is there. Otherwise return
343        None. field is the key name, not the label. Return a copy if requested,
344        just in case the user wants to change values in the dict.
345        '''
346        if field in field_metadata and field_metadata[field]['kind'] == 'field':
347            if make_copy:
348                return copy.deepcopy(field_metadata[field])
349            return field_metadata[field]
350        return None
351
352    def get_all_standard_metadata(self, make_copy):
353        '''
354        return a dict containing all the standard field metadata associated with
355        the book.
356        '''
357        if not make_copy:
358            return field_metadata
359        res = {}
360        for k in field_metadata:
361            if field_metadata[k]['kind'] == 'field':
362                res[k] = copy.deepcopy(field_metadata[k])
363        return res
364
365    def get_all_user_metadata(self, make_copy):
366        '''
367        return a dict containing all the custom field metadata associated with
368        the book.
369        '''
370        _data = object.__getattribute__(self, '_data')
371        user_metadata = _data['user_metadata']
372        if not make_copy:
373            return user_metadata
374        res = {}
375        for k in user_metadata:
376            res[k] = copy.deepcopy(user_metadata[k])
377        return res
378
379    def get_user_metadata(self, field, make_copy):
380        '''
381        return field metadata from the object if it is there. Otherwise return
382        None. field is the key name, not the label. Return a copy if requested,
383        just in case the user wants to change values in the dict.
384        '''
385        _data = object.__getattribute__(self, '_data')
386        _data = _data['user_metadata']
387        if field in _data:
388            if make_copy:
389                return copy.deepcopy(_data[field])
390            return _data[field]
391        return None
392
393    def set_all_user_metadata(self, metadata):
394        '''
395        store custom field metadata into the object. Field is the key name
396        not the label
397        '''
398        if metadata is None:
399            traceback.print_stack()
400            return
401
402        um = {}
403        for key, meta in iteritems(metadata):
404            m = meta.copy()
405            if '#value#' not in m:
406                if m['datatype'] == 'text' and m['is_multiple']:
407                    m['#value#'] = []
408                else:
409                    m['#value#'] = None
410            um[key] = m
411        _data = object.__getattribute__(self, '_data')
412        _data['user_metadata'] = um
413
414    def set_user_metadata(self, field, metadata):
415        '''
416        store custom field metadata for one column into the object. Field is
417        the key name not the label
418        '''
419        if field is not None:
420            if not field.startswith('#'):
421                raise AttributeError(
422                        'Custom field name %s must begin with \'#\''%repr(field))
423            if metadata is None:
424                traceback.print_stack()
425                return
426            m = dict(metadata)
427            # Copying the elements should not be necessary. The objects referenced
428            # in the dict should not change. Of course, they can be replaced.
429            # for k,v in iteritems(metadata):
430            #     m[k] = copy.copy(v)
431            if '#value#' not in m:
432                if m['datatype'] == 'text' and m['is_multiple']:
433                    m['#value#'] = []
434                else:
435                    m['#value#'] = None
436            _data = object.__getattribute__(self, '_data')
437            _data['user_metadata'][field] = m
438
439    def remove_stale_user_metadata(self, other_mi):
440        '''
441        Remove user metadata keys (custom column keys) if they
442        don't exist in 'other_mi', which must be a metadata object
443        '''
444        me = self.get_all_user_metadata(make_copy=False)
445        other = set(other_mi.custom_field_keys())
446        new = {}
447        for k,v in me.items():
448            if k in other:
449                new[k] = v
450        self.set_all_user_metadata(new)
451
452    def template_to_attribute(self, other, ops):
453        '''
454        Takes a list [(src,dest), (src,dest)], evaluates the template in the
455        context of other, then copies the result to self[dest]. This is on a
456        best-efforts basis. Some assignments can make no sense.
457        '''
458        if not ops:
459            return
460        from calibre.ebooks.metadata.book.formatter import SafeFormat
461        formatter = SafeFormat()
462        for op in ops:
463            try:
464                src = op[0]
465                dest = op[1]
466                val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
467                if dest == 'tags':
468                    self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
469                elif dest == 'authors':
470                    self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
471                else:
472                    self.set(dest, val)
473            except:
474                if DEBUG:
475                    traceback.print_exc()
476
477    # Old Metadata API {{{
478    def print_all_attributes(self):
479        for x in STANDARD_METADATA_FIELDS:
480            prints('%s:'%x, getattr(self, x, 'None'))
481        for x in self.custom_field_keys():
482            meta = self.get_user_metadata(x, make_copy=False)
483            if meta is not None:
484                prints(x, meta)
485        prints('--------------')
486
487    def smart_update(self, other, replace_metadata=False):
488        '''
489        Merge the information in `other` into self. In case of conflicts, the information
490        in `other` takes precedence, unless the information in `other` is NULL.
491        '''
492        def copy_not_none(dest, src, attr):
493            v = getattr(src, attr, None)
494            if v not in (None, NULL_VALUES.get(attr, None)):
495                setattr(dest, attr, copy.deepcopy(v))
496
497        unknown = _('Unknown')
498        if other.title and other.title != unknown:
499            self.title = other.title
500            if hasattr(other, 'title_sort'):
501                self.title_sort = other.title_sort
502
503        if other.authors and (
504                other.authors[0] != unknown or (
505                    not self.authors or (
506                        len(self.authors) == 1 and self.authors[0] == unknown and
507                        getattr(self, 'author_sort', None) == unknown
508                    )
509                )
510        ):
511            self.authors = list(other.authors)
512            if hasattr(other, 'author_sort_map'):
513                self.author_sort_map = dict(other.author_sort_map)
514            if hasattr(other, 'author_sort'):
515                self.author_sort = other.author_sort
516
517        if replace_metadata:
518            # SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
519            for attr in SC_COPYABLE_FIELDS:
520                setattr(self, attr, getattr(other, attr, 1.0 if
521                        attr == 'series_index' else None))
522            self.tags = other.tags
523            self.cover_data = getattr(other, 'cover_data',
524                                      NULL_VALUES['cover_data'])
525            self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
526            for x in SC_FIELDS_COPY_NOT_NULL:
527                copy_not_none(self, other, x)
528            if callable(getattr(other, 'get_identifiers', None)):
529                self.set_identifiers(other.get_identifiers())
530            # language is handled below
531        else:
532            for attr in SC_COPYABLE_FIELDS:
533                copy_not_none(self, other, attr)
534            for x in SC_FIELDS_COPY_NOT_NULL:
535                copy_not_none(self, other, x)
536
537            if other.tags:
538                # Case-insensitive but case preserving merging
539                lotags = [t.lower() for t in other.tags]
540                lstags = [t.lower() for t in self.tags]
541                ot, st = map(frozenset, (lotags, lstags))
542                for t in st.intersection(ot):
543                    sidx = lstags.index(t)
544                    oidx = lotags.index(t)
545                    self.tags[sidx] = other.tags[oidx]
546                self.tags += [t for t in other.tags if t.lower() in ot-st]
547
548            if getattr(other, 'cover_data', False):
549                other_cover = other.cover_data[-1]
550                self_cover = self.cover_data[-1] if self.cover_data else b''
551                if not self_cover:
552                    self_cover = b''
553                if not other_cover:
554                    other_cover = b''
555                if len(other_cover) > len(self_cover):
556                    self.cover_data = other.cover_data
557
558            if callable(getattr(other, 'custom_field_keys', None)):
559                for x in other.custom_field_keys():
560                    meta = other.get_user_metadata(x, make_copy=True)
561                    if meta is not None:
562                        self_tags = self.get(x, [])
563                        if isinstance(self_tags, string_or_bytes):
564                            self_tags = []
565                        self.set_user_metadata(x, meta)  # get... did the deepcopy
566                        other_tags = other.get(x, [])
567                        if meta['datatype'] == 'text' and meta['is_multiple']:
568                            # Case-insensitive but case preserving merging
569                            lotags = [t.lower() for t in other_tags]
570                            try:
571                                lstags = [t.lower() for t in self_tags]
572                            except TypeError:
573                                # Happens if x is not a text, is_multiple field
574                                # on self
575                                lstags = []
576                                self_tags = []
577                            ot, st = map(frozenset, (lotags, lstags))
578                            for t in st.intersection(ot):
579                                sidx = lstags.index(t)
580                                oidx = lotags.index(t)
581                                self_tags[sidx] = other_tags[oidx]
582                            self_tags += [t for t in other_tags if t.lower() in ot-st]
583                            setattr(self, x, self_tags)
584
585            my_comments = getattr(self, 'comments', '')
586            other_comments = getattr(other, 'comments', '')
587            if not my_comments:
588                my_comments = ''
589            if not other_comments:
590                other_comments = ''
591            if len(other_comments.strip()) > len(my_comments.strip()):
592                self.comments = other_comments
593
594            # Copy all the non-none identifiers
595            if callable(getattr(other, 'get_identifiers', None)):
596                d = self.get_identifiers()
597                s = other.get_identifiers()
598                d.update([v for v in iteritems(s) if v[1] is not None])
599                self.set_identifiers(d)
600            else:
601                # other structure not Metadata. Copy the top-level identifiers
602                for attr in TOP_LEVEL_IDENTIFIERS:
603                    copy_not_none(self, other, attr)
604
605        other_lang = getattr(other, 'languages', [])
606        if other_lang and other_lang != ['und']:
607            self.languages = list(other_lang)
608        if not getattr(self, 'series', None):
609            self.series_index = None
610
611    def format_series_index(self, val=None):
612        from calibre.ebooks.metadata import fmt_sidx
613        v = self.series_index if val is None else val
614        try:
615            x = float(v)
616        except Exception:
617            x = 1
618        return fmt_sidx(x)
619
620    def authors_from_string(self, raw):
621        from calibre.ebooks.metadata import string_to_authors
622        self.authors = string_to_authors(raw)
623
624    def format_authors(self):
625        from calibre.ebooks.metadata import authors_to_string
626        return authors_to_string(self.authors)
627
628    def format_tags(self):
629        return ', '.join([str(t) for t in sorted(self.tags, key=sort_key)])
630
631    def format_rating(self, v=None, divide_by=1):
632        if v is None:
633            if self.rating is not None:
634                return str(self.rating/divide_by)
635            return 'None'
636        return str(v/divide_by)
637
638    def format_field(self, key, series_with_index=True):
639        '''
640        Returns the tuple (display_name, formatted_value)
641        '''
642        name, val, ign, ign = self.format_field_extended(key, series_with_index)
643        return (name, val)
644
645    def format_field_extended(self, key, series_with_index=True):
646        from calibre.ebooks.metadata import authors_to_string
647        '''
648        returns the tuple (display_name, formatted_value, original_value,
649        field_metadata)
650        '''
651        from calibre.utils.date import format_date
652
653        # Handle custom series index
654        if key.startswith('#') and key.endswith('_index'):
655            tkey = key[:-6]  # strip the _index
656            cmeta = self.get_user_metadata(tkey, make_copy=False)
657            if cmeta and cmeta['datatype'] == 'series':
658                if self.get(tkey):
659                    res = self.get_extra(tkey)
660                    return (str(cmeta['name']+'_index'),
661                            self.format_series_index(res), res, cmeta)
662                else:
663                    return (str(cmeta['name']+'_index'), '', '', cmeta)
664
665        if key in self.custom_field_keys():
666            res = self.get(key, None)       # get evaluates all necessary composites
667            cmeta = self.get_user_metadata(key, make_copy=False)
668            name = str(cmeta['name'])
669            if res is None or res == '':    # can't check "not res" because of numeric fields
670                return (name, res, None, None)
671            orig_res = res
672            datatype = cmeta['datatype']
673            if datatype == 'text' and cmeta['is_multiple']:
674                res = cmeta['is_multiple']['list_to_ui'].join(res)
675            elif datatype == 'series' and series_with_index:
676                if self.get_extra(key) is not None:
677                    res = res + \
678                        ' [%s]'%self.format_series_index(val=self.get_extra(key))
679            elif datatype == 'datetime':
680                res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
681            elif datatype == 'bool':
682                res = _('Yes') if res else _('No')
683            elif datatype == 'rating':
684                res = '%.2g'%(res/2)
685            elif datatype in ['int', 'float']:
686                try:
687                    fmt = cmeta['display'].get('number_format', None)
688                    res = fmt.format(res)
689                except:
690                    pass
691            return (name, str(res), orig_res, cmeta)
692
693        # convert top-level ids into their value
694        if key in TOP_LEVEL_IDENTIFIERS:
695            fmeta = field_metadata['identifiers']
696            name = key
697            res = self.get(key, None)
698            return (name, res, res, fmeta)
699
700        # Translate aliases into the standard field name
701        fmkey = field_metadata.search_term_to_field_key(key)
702        if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
703            res = self.get(key, None)
704            fmeta = field_metadata[fmkey]
705            name = str(fmeta['name'])
706            if res is None or res == '':
707                return (name, res, None, None)
708            orig_res = res
709            name = str(fmeta['name'])
710            datatype = fmeta['datatype']
711            if key == 'authors':
712                res = authors_to_string(res)
713            elif key == 'series_index':
714                res = self.format_series_index(res)
715            elif datatype == 'text' and fmeta['is_multiple']:
716                if isinstance(res, dict):
717                    res = [k + ':' + v for k,v in res.items()]
718                res = fmeta['is_multiple']['list_to_ui'].join(sorted(filter(None, res), key=sort_key))
719            elif datatype == 'series' and series_with_index:
720                res = res + ' [%s]'%self.format_series_index()
721            elif datatype == 'datetime':
722                res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
723            elif datatype == 'rating':
724                res = '%.2g'%(res/2)
725            elif key == 'size':
726                res = human_readable(res)
727            return (name, str(res), orig_res, fmeta)
728
729        return (None, None, None, None)
730
731    def __unicode__representation__(self):
732        '''
733        A string representation of this object, suitable for printing to
734        console
735        '''
736        from calibre.utils.date import isoformat
737        from calibre.ebooks.metadata import authors_to_string
738        ans = []
739
740        def fmt(x, y):
741            ans.append('%-20s: %s'%(str(x), str(y)))
742
743        fmt('Title', self.title)
744        if self.title_sort:
745            fmt('Title sort', self.title_sort)
746        if self.authors:
747            fmt('Author(s)',  authors_to_string(self.authors) +
748               ((' [' + self.author_sort + ']')
749                if self.author_sort and self.author_sort != _('Unknown') else ''))
750        if self.publisher:
751            fmt('Publisher', self.publisher)
752        if getattr(self, 'book_producer', False):
753            fmt('Book Producer', self.book_producer)
754        if self.tags:
755            fmt('Tags', ', '.join([str(t) for t in self.tags]))
756        if self.series:
757            fmt('Series', self.series + ' #%s'%self.format_series_index())
758        if not self.is_null('languages'):
759            fmt('Languages', ', '.join(self.languages))
760        if self.rating is not None:
761            fmt('Rating', ('%.2g'%(float(self.rating)/2)) if self.rating
762                    else '')
763        if self.timestamp is not None:
764            fmt('Timestamp', isoformat(self.timestamp))
765        if self.pubdate is not None:
766            fmt('Published', isoformat(self.pubdate))
767        if self.rights is not None:
768            fmt('Rights', str(self.rights))
769        if self.identifiers:
770            fmt('Identifiers', ', '.join(['%s:%s'%(k, v) for k, v in
771                iteritems(self.identifiers)]))
772        if self.comments:
773            fmt('Comments', self.comments)
774
775        for key in self.custom_field_keys():
776            val = self.get(key, None)
777            if val:
778                (name, val) = self.format_field(key)
779                fmt(name, str(val))
780        return '\n'.join(ans)
781
782    def to_html(self):
783        '''
784        A HTML representation of this object.
785        '''
786        from calibre.ebooks.metadata import authors_to_string
787        from calibre.utils.date import isoformat
788        ans = [(_('Title'), str(self.title))]
789        ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
790        ans += [(_('Publisher'), str(self.publisher))]
791        ans += [(_('Producer'), str(self.book_producer))]
792        ans += [(_('Comments'), str(self.comments))]
793        ans += [('ISBN', str(self.isbn))]
794        ans += [(_('Tags'), ', '.join([str(t) for t in self.tags]))]
795        if self.series:
796            ans += [(ngettext('Series', 'Series', 1), str(self.series) + ' #%s'%self.format_series_index())]
797        ans += [(_('Languages'), ', '.join(self.languages))]
798        if self.timestamp is not None:
799            ans += [(_('Timestamp'), str(isoformat(self.timestamp, as_utc=False, sep=' ')))]
800        if self.pubdate is not None:
801            ans += [(_('Published'), str(isoformat(self.pubdate, as_utc=False, sep=' ')))]
802        if self.rights is not None:
803            ans += [(_('Rights'), str(self.rights))]
804        for key in self.custom_field_keys():
805            val = self.get(key, None)
806            if val:
807                (name, val) = self.format_field(key)
808                ans += [(name, val)]
809        for i, x in enumerate(ans):
810            ans[i] = '<tr><td><b>%s</b></td><td>%s</td></tr>'%x
811        return '<table>%s</table>'%'\n'.join(ans)
812
813    __str__ = __unicode__representation__
814
815    def __nonzero__(self):
816        return bool(self.title or self.author or self.comments or self.tags)
817    __bool__ = __nonzero__
818
819    # }}}
820
821
822def field_from_string(field, raw, field_metadata):
823    ''' Parse the string raw to return an object that is suitable for calling
824    set() on a Metadata object. '''
825    dt = field_metadata['datatype']
826    val = object
827    if dt in {'int', 'float'}:
828        val = int(raw) if dt == 'int' else float(raw)
829    elif dt == 'rating':
830        val = float(raw) * 2
831    elif dt == 'datetime':
832        from calibre.utils.iso8601 import parse_iso8601
833        try:
834            val = parse_iso8601(raw, require_aware=True)
835        except Exception:
836            from calibre.utils.date import parse_only_date
837            val = parse_only_date(raw)
838    elif dt == 'bool':
839        if raw.lower() in {'true', 'yes', 'y'}:
840            val = True
841        elif raw.lower() in {'false', 'no', 'n'}:
842            val = False
843        else:
844            raise ValueError('Unknown value for %s: %s'%(field, raw))
845    elif dt == 'text':
846        ism = field_metadata['is_multiple']
847        if ism:
848            val = [x.strip() for x in raw.split(ism['ui_to_list'])]
849            if field == 'identifiers':
850                val = {x.partition(':')[0]:x.partition(':')[-1] for x in val}
851            elif field == 'languages':
852                from calibre.utils.localization import canonicalize_lang
853                val = [canonicalize_lang(x) for x in val]
854                val = [x for x in val if x]
855    if val is object:
856        val = raw
857    return val
858