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