E-book viewer: When displaying metadata for the book, also display custom column metadata

This commit is contained in:
Kovid Goyal 2014-02-22 16:43:52 +05:30
parent 5da59094c9
commit 225a7e0723
3 changed files with 236 additions and 175 deletions

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__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
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
from calibre.constants import filesystem_encoding
from calibre.library.comments import comments_to_html
from calibre.utils.icu import sort_key
from calibre.utils.formatter import EvalFormatter
from calibre.utils.date import is_date_undefined
from calibre.utils.localization import calibre_langcode_to_name
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['kind'] == 'field' and m['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 mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif'):
if field_list is None:
field_list = get_field_list(mi)
ans = []
comment_fields = []
isdevice = not hasattr(mi, 'id')
row = u'<td class="title">%s</td><td class="value">%s</td>'
p = prepare_string_for_xml
a = partial(prepare_string_for_xml, attribute=True)
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['datatype'] == 'bool':
isnull = mi.get(field) is None
else:
isnull = mi.is_null(field)
if isnull:
continue
name = metadata['name']
if not name:
name = field
name += ':'
if metadata['datatype'] == 'comments' or field == 'comments':
val = getattr(mi, field)
if val:
val = force_unicode(val)
comment_fields.append(comments_to_html(val))
elif metadata['datatype'] == 'rating':
val = getattr(mi, field)
if val:
val = val/2.0
ans.append((field,
u'<td class="title">%s</td><td class="rating value" '
'style=\'font-family:"%s"\'>%s</td>'%(
name, rating_font, u'\u2605'*int(val))))
elif metadata['datatype'] == 'composite' and \
metadata['display'].get('contains_html', False):
val = getattr(mi, field)
if val:
val = force_unicode(val)
ans.append((field,
row % (name, comments_to_html(val))))
elif field == 'path':
if mi.path:
path = force_unicode(mi.path, filesystem_encoding)
scheme = u'devpath' if isdevice else u'path'
url = prepare_string_for_xml(path if isdevice else
unicode(mi.id), True)
pathstr = _('Click to open')
extra = ''
if isdevice:
durl = url
if durl.startswith('mtp:::'):
durl = ':::'.join((durl.split(':::'))[2:])
extra = '<br><span style="font-size:smaller">%s</span>'%(
prepare_string_for_xml(durl))
link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url,
prepare_string_for_xml(path, True), pathstr, extra)
ans.append((field, row % (name, link)))
elif field == 'formats':
if isdevice:
continue
path = ''
if mi.path:
h, t = os.path.split(mi.path)
path = '/'.join((os.path.basename(h), t))
data = ({
'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')),
'ext':x.lower(), 'id':mi.id
} for x in mi.formats)
fmts = [u'<a title="{path}/{fname}.{ext}" href="format:{id}:{fmt}">{fmt}</a>'.format(**x) for x in data]
ans.append((field, row % (name, u', '.join(fmts))))
elif field == 'identifiers':
urls = urls_from_identifiers(mi.identifiers)
links = [u'<a href="%s" title="%s:%s">%s</a>' % (a(url), a(id_typ), a(id_val), p(name))
for name, id_typ, id_val, url in urls]
links = u', '.join(links)
if links:
ans.append((field, row % (_('Ids')+':', links)))
elif field == 'authors' and not isdevice:
authors = []
formatter = EvalFormatter()
for aut in mi.authors:
link = ''
if mi.author_link_map[aut]:
link = mi.author_link_map[aut]
elif default_author_link:
vals = {'author': aut.replace(' ', '+')}
try:
vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+')
except:
vals['author_sort'] = aut.replace(' ', '+')
link = formatter.safe_format(
default_author_link, vals, '', vals)
aut = p(aut)
if link:
authors.append(u'<a calibre-data="authors" href="%s">%s</a>'%(a(link), aut))
else:
authors.append(aut)
ans.append((field, row % (name, u' & '.join(authors))))
elif field == 'languages':
if not mi.languages:
continue
names = filter(None, map(calibre_langcode_to_name, mi.languages))
ans.append((field, row % (name, u', '.join(names))))
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
val = _('Book %(sidx)s of <span class="series_name">%(series)s</span>')%dict(
sidx=fmt_sidx(sidx, use_roman=use_roman_numbers),
series=p(getattr(mi, field)))
elif metadata['datatype'] == 'datetime':
aval = getattr(mi, field)
if is_date_undefined(aval):
continue
ans.append((field, row % (name, val)))
dc = getattr(mi, 'device_collections', [])
if dc:
dc = u', '.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 = [u'<tr id="%s" class="%s">%s</tr>'%(field.replace('#', '_'),
classname(field), html) for field, html in ans]
# print '\n'.join(ans)
return u'<table class="fields">%s</table>'%(u'\n'.join(ans)), comment_fields

View File

@ -5,34 +5,24 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction,
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu,
QPen, QColor)
from PyQt4.QtWebKit import QWebView
from calibre import fit_image, force_unicode, prepare_string_for_xml
from calibre import fit_image
from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
IMAGE_EXTENSIONS, dnd_has_extension)
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.book.base import (field_metadata, Metadata)
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
from calibre.constants import filesystem_encoding
from calibre.library.comments import comments_to_html
from calibre.ebooks.metadata.book.render import mi_to_html
from calibre.gui2 import (config, open_url, pixmap_to_data, gprefs,
rating_font)
from calibre.utils.icu import sort_key
from calibre.utils.formatter import EvalFormatter
from calibre.utils.date import is_date_undefined
from calibre.utils.localization import calibre_langcode_to_name
from calibre.utils.config import tweaks
def render_html(mi, css, vertical, widget, all_fields=False): # {{{
table, comment_fields = render_data(mi, all_fields=all_fields,
def render_html(mi, css, vertical, widget, all_fields=False, render_data_func=None): # {{{
table, comment_fields = (render_data_func or render_data)(mi, all_fields=all_fields,
use_roman_numbers=config['use_roman_numerals_for_series_number'])
def color_to_string(col):
@ -105,149 +95,10 @@ def get_field_list(fm, use_defaults=False):
return [(f, d) for f, d in fieldlist if f in available]
def render_data(mi, use_roman_numbers=True, all_fields=False):
ans = []
comment_fields = []
isdevice = not hasattr(mi, 'id')
fm = getattr(mi, 'field_metadata', field_metadata)
row = u'<td class="title">%s</td><td class="value">%s</td>'
p = prepare_string_for_xml
a = partial(prepare_string_for_xml, attribute=True)
for field, display in get_field_list(fm):
metadata = fm.get(field, None)
if field == 'sort':
field = 'title_sort'
if all_fields:
display = True
if metadata['datatype'] == 'bool':
isnull = mi.get(field) is None
else:
isnull = mi.is_null(field)
if (not display or not metadata or isnull):
continue
name = metadata['name']
if not name:
name = field
name += ':'
if metadata['datatype'] == 'comments' or field == 'comments':
val = getattr(mi, field)
if val:
val = force_unicode(val)
comment_fields.append(comments_to_html(val))
elif metadata['datatype'] == 'rating':
val = getattr(mi, field)
if val:
val = val/2.0
ans.append((field,
u'<td class="title">%s</td><td class="rating value" '
'style=\'font-family:"%s"\'>%s</td>'%(
name, rating_font(), u'\u2605'*int(val))))
elif metadata['datatype'] == 'composite' and \
metadata['display'].get('contains_html', False):
val = getattr(mi, field)
if val:
val = force_unicode(val)
ans.append((field,
row % (name, comments_to_html(val))))
elif field == 'path':
if mi.path:
path = force_unicode(mi.path, filesystem_encoding)
scheme = u'devpath' if isdevice else u'path'
url = prepare_string_for_xml(path if isdevice else
unicode(mi.id), True)
pathstr = _('Click to open')
extra = ''
if isdevice:
durl = url
if durl.startswith('mtp:::'):
durl = ':::'.join((durl.split(':::'))[2:])
extra = '<br><span style="font-size:smaller">%s</span>'%(
prepare_string_for_xml(durl))
link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url,
prepare_string_for_xml(path, True), pathstr, extra)
ans.append((field, row % (name, link)))
elif field == 'formats':
if isdevice:
continue
path = ''
if mi.path:
h, t = os.path.split(mi.path)
path = '/'.join((os.path.basename(h), t))
data = ({
'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')),
'ext':x.lower(), 'id':mi.id
} for x in mi.formats)
fmts = [u'<a title="{path}/{fname}.{ext}" href="format:{id}:{fmt}">{fmt}</a>'.format(**x) for x in data]
ans.append((field, row % (name, u', '.join(fmts))))
elif field == 'identifiers':
urls = urls_from_identifiers(mi.identifiers)
links = [u'<a href="%s" title="%s:%s">%s</a>' % (a(url), a(id_typ), a(id_val), p(name))
for name, id_typ, id_val, url in urls]
links = u', '.join(links)
if links:
ans.append((field, row % (_('Ids')+':', links)))
elif field == 'authors' and not isdevice:
authors = []
formatter = EvalFormatter()
for aut in mi.authors:
link = ''
if mi.author_link_map[aut]:
link = mi.author_link_map[aut]
elif gprefs.get('default_author_link'):
vals = {'author': aut.replace(' ', '+')}
try:
vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+')
except:
vals['author_sort'] = aut.replace(' ', '+')
link = formatter.safe_format(
gprefs.get('default_author_link'), vals, '', vals)
aut = p(aut)
if link:
authors.append(u'<a calibre-data="authors" href="%s">%s</a>'%(a(link), aut))
else:
authors.append(aut)
ans.append((field, row % (name, u' & '.join(authors))))
elif field == 'languages':
if not mi.languages:
continue
names = filter(None, map(calibre_langcode_to_name, mi.languages))
ans.append((field, row % (name, u', '.join(names))))
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
val = _('Book %(sidx)s of <span class="series_name">%(series)s</span>')%dict(
sidx=fmt_sidx(sidx, use_roman=use_roman_numbers),
series=p(getattr(mi, field)))
elif metadata['datatype'] == 'datetime':
aval = getattr(mi, field)
if is_date_undefined(aval):
continue
ans.append((field, row % (name, val)))
dc = getattr(mi, 'device_collections', [])
if dc:
dc = u', '.join(sorted(dc, key=sort_key))
ans.append(('device_collections',
row % (_('Collections')+':', dc)))
def classname(field):
try:
dt = fm[field]['datatype']
except:
dt = 'text'
return 'datatype_%s'%dt
ans = [u'<tr id="%s" class="%s">%s</tr>'%(field.replace('#', '_'),
classname(field), html) for field, html in ans]
# print '\n'.join(ans)
return u'<table class="fields">%s</table>'%(u'\n'.join(ans)), comment_fields
field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata))
field_list = [(x, all_fields or display) for x, display in field_list]
return mi_to_html(mi, field_list=field_list, use_roman_numbers=use_roman_numbers,
rating_font=rating_font(), default_author_link=gprefs.get('default_author_link'))
# }}}

View File

@ -5,11 +5,12 @@ import traceback, os, sys, functools, textwrap
from functools import partial
from threading import Thread
from PyQt4.Qt import (QApplication, Qt, QIcon, QTimer, QByteArray, QSize,
QTime, QDoubleSpinBox, QLabel, QTextBrowser, QPropertyAnimation,
QPainter, QBrush, QColor, pyqtSignal, QUrl, QRegExpValidator, QRegExp,
QLineEdit, QToolButton, QMenu, QInputDialog, QAction,
QModelIndex)
from PyQt4.Qt import (
QApplication, Qt, QIcon, QTimer, QByteArray, QSize, QTime, QDoubleSpinBox,
QLabel, QPropertyAnimation, pyqtSignal, QUrl, QRegExpValidator, QRegExp,
QLineEdit, QToolButton, QMenu, QInputDialog, QAction, QModelIndex, QPalette,
QPainter, QBrush, QColor)
from PyQt4.QtWebKit import QWebView
from calibre.gui2.viewer.main_ui import Ui_EbookViewer
from calibre.gui2.viewer.printing import Printing
@ -17,14 +18,13 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.viewer.toc import TOC
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2.main_window import MainWindow
from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files,
from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files, rating_font,
info_dialog, error_dialog, open_url, available_height, setup_gui_option_parser, detach_gui)
from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.gui2.search_box import SearchBox2
from calibre.ebooks.metadata import MetaInformation
from calibre.customize.ui import available_input_formats
from calibre.gui2.viewer.dictionary import Lookup
from calibre import as_unicode, force_unicode, isbytestring
@ -102,31 +102,44 @@ class History(list):
self.forward_pos = None
self.set_actions()
class Metadata(QLabel):
class Metadata(QWebView):
def __init__(self, parent):
QTextBrowser.__init__(self, parent.centralWidget())
QWebView.__init__(self, parent.centralWidget())
s = self.settings()
s.setAttribute(s.JavascriptEnabled, False)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
self.css = P('templates/book_details.css', data=True).decode('utf-8')
self.view = parent.splitter
self.setGeometry(self.view.geometry())
self.setWordWrap(True)
self.setVisible(False)
def show_opf(self, opf, ext=''):
mi = MetaInformation(opf)
html = '<h2 align="center">%s</h2>%s\n<b>%s:</b> %s'\
%(_('Metadata'), u''.join(mi.to_html()),
_('Book format'), ext.upper())
self.setText(html)
from calibre.gui2.book_details import render_html
from calibre.ebooks.metadata.book.render import mi_to_html
def render_data(mi, use_roman_numbers=True, all_fields=False):
return mi_to_html(mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font())
mi = opf.to_book_metadata()
html = render_html(mi, self.css, True, self, render_data_func=render_data)
self.setHtml(html)
def setVisible(self, x):
self.setGeometry(self.view.geometry())
QLabel.setVisible(self, x)
if x:
self.setGeometry(self.view.geometry())
QWebView.setVisible(self, x)
def paintEvent(self, ev):
p = QPainter(self)
p.fillRect(ev.region().boundingRect(), QBrush(QColor(200, 200, 200, 220), Qt.SolidPattern))
p.end()
QLabel.paintEvent(self, ev)
QWebView.paintEvent(self, ev)
class DoubleSpinBox(QDoubleSpinBox):