%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/ebooks/metadata/book/ |
Current File : //usr/lib/calibre/calibre/ebooks/metadata/book/render.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' import os from functools import partial from calibre import prepare_string_for_xml, force_unicode from calibre.ebooks.metadata import fmt_sidx, rating_to_stars from calibre.ebooks.metadata.search_internet import name_for, url_for_author_search, url_for_book_search, qquote, DEFAULT_AUTHOR_SOURCE from calibre.ebooks.metadata.sources.identify import urls_from_identifiers from calibre.constants import filesystem_encoding from calibre.library.comments import comments_to_html, markdown from calibre.utils.icu import sort_key from calibre.utils.formatter import EvalFormatter from calibre.utils.date import is_date_undefined, format_date from calibre.utils.localization import calibre_langcode_to_name from calibre.utils.serialize import json_dumps from polyglot.binary import as_hex_unicode default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') def field_sort(mi, name): try: title = mi.metadata_for_field(name)['name'] except: title = 'zzz' return {x:(i, None) for i, x in enumerate(default_sort)}.get(name, (10000, sort_key(title))) def displayable_field_keys(mi): for k in mi.all_field_keys(): try: m = mi.metadata_for_field(k) except: continue if ( m is not None and m.get('kind') == 'field' and m.get('datatype') is not None and k not in ('au_map', 'marked', 'ondevice', 'cover', 'series_sort') and not k.endswith('_index') ): yield k def get_field_list(mi): for field in sorted(displayable_field_keys(mi), key=partial(field_sort, mi)): yield field, True def action(main, **keys): keys['type'] = main return 'action:' + as_hex_unicode(json_dumps(keys)) def search_action(search_term, value, **k): return action('search', term=search_term, value=value, **k) def search_action_with_data(search_term, value, book_id, field=None, **k): field = field or search_term return search_action(search_term, value, field=field, book_id=book_id, **k) DEFAULT_AUTHOR_LINK = f'search-{DEFAULT_AUTHOR_SOURCE}' def author_search_href(which, title=None, author=None): if which == 'calibre': return 'calibre', _('Search the calibre library for books by %s') % author search_type, key = 'author', which if which.endswith('-book'): key, search_type = which.rpartition('-')[::2] name = name_for(key) if name is None: search_type = 'author' return author_search_href(DEFAULT_AUTHOR_LINK.partition('-')[2], title=title, author=author) if search_type == 'author': tt = _('Search {0} for the author: {1}').format(name, author) else: tt = _('Search {0} for the book: {1} by the author {2}').format(name, title, author) func = url_for_book_search if search_type == 'book' else url_for_author_search return func(key, title=title, author=author), tt def mi_to_html( mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif', rtl=False, comments_heading_pos='hide', for_qt=False, ): if field_list is None: field_list = get_field_list(mi) ans = [] comment_fields = [] isdevice = not hasattr(mi, 'id') row = '<td class="title">%s</td><td class="value">%s</td>' p = prepare_string_for_xml a = partial(prepare_string_for_xml, attribute=True) book_id = getattr(mi, 'id', 0) for field in (field for field, display in field_list if display): try: metadata = mi.metadata_for_field(field) except: continue if not metadata: continue if field == 'sort': field = 'title_sort' if metadata['is_custom'] and metadata['datatype'] in {'bool', 'int', 'float'}: isnull = mi.get(field) is None else: isnull = mi.is_null(field) if isnull: continue name = metadata['name'] if not name: name = field name += ':' disp = metadata['display'] if metadata['datatype'] == 'comments' or field == 'comments': val = getattr(mi, field) if val: ctype = disp.get('interpret_as') or 'html' val = force_unicode(val) if ctype == 'long-text': val = '<pre style="white-space:pre-wrap">%s</pre>' % p(val) elif ctype == 'short-text': val = '<span>%s</span>' % p(val) elif ctype == 'markdown': val = markdown(val) else: val = comments_to_html(val) heading_position = disp.get('heading_position', comments_heading_pos) if heading_position == 'side': ans.append((field, row % (name, val))) else: if heading_position == 'above': val = f'<h3 class="comments-heading">{p(name)}</h3>{val}' comment_fields.append('<div id="{}" class="comments">{}</div>'.format(field.replace('#', '_'), val)) elif metadata['datatype'] == 'rating': val = getattr(mi, field) if val: star_string = rating_to_stars(val, disp.get('allow_half_stars', False)) ans.append((field, '<td class="title">%s</td><td class="rating value" ' 'style=\'font-family:"%s"\'>%s</td>'%( name, rating_font, star_string))) elif metadata['datatype'] == 'composite': val = getattr(mi, field) if val: val = force_unicode(val) if disp.get('contains_html', False): ans.append((field, row % (name, comments_to_html(val)))) else: if not metadata['is_multiple']: val = '<a href="{}" title="{}">{}</a>'.format( search_action(field, val), _('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val)) else: all_vals = [v.strip() for v in val.split(metadata['is_multiple']['list_to_ui']) if v.strip()] links = ['<a href="{}" title="{}">{}</a>'.format( search_action(field, x), _('Click to see books with {0}: {1}').format( metadata['name'], a(x)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) ans.append((field, row % (name, val))) elif field == 'path': if mi.path: path = force_unicode(mi.path, filesystem_encoding) scheme = 'devpath' if isdevice else 'path' loc = path if isdevice else book_id pathstr = _('Click to open') extra = '' if isdevice: durl = path if durl.startswith('mtp:::'): durl = ':::'.join((durl.split(':::'))[2:]) extra = '<br><span style="font-size:smaller">%s</span>'%( prepare_string_for_xml(durl)) link = '<a href="{}" title="{}">{}</a>{}'.format(action(scheme, loc=loc), prepare_string_for_xml(path, True), pathstr, extra) ans.append((field, row % (name, link))) elif field == 'formats': if isdevice: continue path = mi.path or '' bpath = '' if path: h, t = os.path.split(path) bpath = os.sep.join((os.path.basename(h), t)) data = ({ 'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), 'ext':x.lower(), 'id':book_id, 'bpath':bpath, 'sep':os.sep, 'action':action('format', book_id=book_id, fmt=x, path=path or '', fname=mi.format_files.get(x, '')) } for x in mi.formats) fmts = ['<a title="{bpath}{sep}{fname}.{ext}" href="{action}">{fmt}</a>'.format(**x) for x in data] ans.append((field, row % (name, ', '.join(fmts)))) elif field == 'identifiers': urls = urls_from_identifiers(mi.identifiers, sort_results=True) links = [ '<a href="{}" title="{}:{}">{}</a>'.format( action('identifier', url=url, name=namel, id_type=id_typ, value=id_val, field='identifiers', book_id=book_id), a(id_typ), a(id_val), p(namel)) for namel, id_typ, id_val, url in urls] links = ', '.join(links) if links: ans.append((field, row % (_('Ids')+':', links))) elif field == 'authors': authors = [] formatter = EvalFormatter() for aut in mi.authors: link = '' if mi.author_link_map.get(aut): link = lt = mi.author_link_map[aut] elif default_author_link: if default_author_link.startswith('search-'): which_src = default_author_link.partition('-')[2] link, lt = author_search_href(which_src, title=mi.title, author=aut) else: vals = {'author': qquote(aut), 'title': qquote(mi.title)} try: vals['author_sort'] = qquote(mi.author_sort_map[aut]) except KeyError: vals['author_sort'] = qquote(aut) link = lt = formatter.safe_format(default_author_link, vals, '', vals) aut = p(aut) if link: authors.append('<a title="%s" href="%s">%s</a>'%(a(lt), action('author', url=link, name=aut, title=lt), aut)) else: authors.append(aut) ans.append((field, row % (name, ' & '.join(authors)))) elif field == 'languages': if not mi.languages: continue names = filter(None, map(calibre_langcode_to_name, mi.languages)) names = ['<a href="{}" title="{}">{}</a>'.format(search_action_with_data('languages', n, book_id), _( 'Search calibre for books with the language: {}').format(n), n) for n in names] ans.append((field, row % (name, ', '.join(names)))) elif field == 'publisher': if not mi.publisher: continue val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data('publisher', mi.publisher, book_id), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), p(mi.publisher)) ans.append((field, row % (name, val))) elif field == 'title': # otherwise title gets metadata['datatype'] == 'text' # treatment below with a click to search link (which isn't # too bad), and a right-click 'Delete' option to delete # the title (which is bad). val = mi.format_field(field)[-1] ans.append((field, row % (name, val))) else: val = mi.format_field(field)[-1] if val is None: continue val = p(val) if metadata['datatype'] == 'series': sidx = mi.get(field+'_index') if sidx is None: sidx = 1.0 try: st = metadata['search_terms'][0] except Exception: st = field series = getattr(mi, field) val = _( '%(sidx)s of <a href="%(href)s" title="%(tt)s">' '<span class="%(cls)s">%(series)s</span></a>') % dict( sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name", series=p(series), href=search_action_with_data(st, series, book_id, field), tt=p(_('Click to see books in this series'))) elif metadata['datatype'] == 'datetime': aval = getattr(mi, field) if is_date_undefined(aval): continue aval = format_date(aval, 'yyyy-MM-dd') if val == aval: val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(field, str(aval), book_id, None, original_value=val), a( _('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val) else: val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(field, str(aval), book_id, None, original_value=val), a( _('Click to see books with {0}: {1} (derived from {2})').format( metadata['name'] or field, aval, val)), val) elif metadata['datatype'] == 'text' and metadata['is_multiple']: try: st = metadata['search_terms'][0] except Exception: st = field all_vals = mi.get(field) if not metadata.get('display', {}).get('is_names', False): all_vals = sorted(all_vals, key=sort_key) links = ['<a href="{}" title="{}">{}</a>'.format( search_action_with_data(st, x, book_id, field), _('Click to see books with {0}: {1}').format( metadata['name'] or field, a(x)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) elif metadata['datatype'] == 'text' or metadata['datatype'] == 'enumeration': # text/is_multiple handled above so no need to add the test to the if try: st = metadata['search_terms'][0] except Exception: st = field val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(st, val, book_id, field), a( _('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), p(val)) elif metadata['datatype'] == 'bool': val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(field, val, book_id, None), a( _('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val) else: try: aval = str(getattr(mi, field)) if not aval: continue if val == aval: val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(field, str(aval), book_id, None, original_value=val), a( _('Click to see books with {0}: {1}').format(metadata['name'] or field, val)), val) else: val = '<a href="{}" title="{}">{}</a>'.format( search_action_with_data(field, str(aval), book_id, None, original_value=val), a( _('Click to see books with {0}: {1} (derived from {2})').format( metadata['name'] or field, aval, val)), val) except Exception: import traceback traceback.print_exc() ans.append((field, row % (name, val))) dc = getattr(mi, 'device_collections', []) if dc: dc = ', '.join(sorted(dc, key=sort_key)) ans.append(('device_collections', row % (_('Collections')+':', dc))) def classname(field): try: dt = mi.metadata_for_field(field)['datatype'] except: dt = 'text' return 'datatype_%s'%dt ans = ['<tr id="%s" class="%s">%s</tr>'%(fieldl.replace('#', '_'), classname(fieldl), html) for fieldl, html in ans] # print '\n'.join(ans) direction = 'rtl' if rtl else 'ltr' rans = '<style>table.fields td { vertical-align:top}</style><table class="fields" style="direction: %s; ' % direction if not for_qt: # This causes wasted space at the edge of the table in Qt's rich text # engine, see https://bugs.launchpad.net/calibre/+bug/1881488 margin = 'left' if rtl else 'right' rans += f'margin-{margin}: auto; ' return '{}">{}</table>'.format(rans, '\n'.join(ans)), comment_fields