Allow creation of rules to convert arbitrary identifiers into clickable links in the book details panel

This commit is contained in:
Kovid Goyal 2016-04-10 19:53:09 +05:30
parent 9269e62723
commit b20610910e
4 changed files with 155 additions and 14 deletions

View File

@ -14,6 +14,7 @@ from threading import Thread
from io import BytesIO
from operator import attrgetter
from urlparse import urlparse
from urllib import quote
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
from calibre.ebooks.metadata import check_issn
@ -25,6 +26,7 @@ from calibre.utils.date import utc_tz, as_utc
from calibre.utils.html2text import html2text
from calibre.utils.icu import lower
from calibre.utils.date import UNDEFINED_DATE
from calibre.utils.formatter import EvalFormatter
# Download worker {{{
class Worker(Thread):
@ -477,9 +479,9 @@ def identify(log, abort, # {{{
if f == 'series':
result.series_index = dummy.series_index
result.relevance_in_source = i
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
and plugin.get_cached_cover_url(result.identifiers) is not
None)
result.has_cached_cover_url = (
plugin.cached_cover_url_is_reliable and
plugin.get_cached_cover_url(result.identifiers) is not None)
result.identify_plugin = plugin
if msprefs['txt_comments']:
if plugin.has_html_comments and result.comments:
@ -524,6 +526,20 @@ def identify(log, abort, # {{{
def urls_from_identifiers(identifiers): # {{{
identifiers = {k.lower():v for k, v in identifiers.iteritems()}
ans = []
rules = msprefs['id_link_rules']
if rules:
formatter = EvalFormatter()
for k, val in identifiers.iteritems():
vals = {'id':quote(val)}
items = rules.get(k) or ()
for name, template in items:
try:
url = formatter.safe_format(template, vals, '', vals)
except Exception:
import traceback
traceback.format_exc()
continue
ans.append((name, k, val, url))
for plugin in all_metadata_plugins():
try:
for id_type, id_val, url in plugin.get_book_urls(identifiers):

View File

@ -20,6 +20,7 @@ msprefs.defaults['fewer_tags'] = True
msprefs.defaults['find_first_edition_date'] = False
msprefs.defaults['append_comments'] = False
msprefs.defaults['tag_map_rules'] = []
msprefs.defaults['id_link_rules'] = {}
# Google covers are often poor quality (scans/errors) but they have high
# resolution, so they trump covers from better sources. So make sure they

View File

@ -7,19 +7,23 @@ __docformat__ = 'restructuredtext en'
import json
from collections import defaultdict
from threading import Thread
from functools import partial
from PyQt5.Qt import (
QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter,
QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, QCursor,
QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton, QVBoxLayout)
QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton, QVBoxLayout,
QTableWidget, QTableWidgetItem, QLabel, QFormLayout, QLineEdit
)
from calibre import human_readable
from calibre.ebooks.metadata.sources.prefs import msprefs
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app, open_local_file, question_dialog
from calibre.gui2 import config, gprefs, qt_app, open_local_file, question_dialog, error_dialog
from calibre.utils.localization import (available_translations,
get_language, get_lang)
from calibre.utils.config import prefs
@ -27,6 +31,7 @@ from calibre.utils.icu import sort_key
from calibre.gui2.book_details import get_field_list
from calibre.gui2.preferences.coloring import EditRules
from calibre.gui2.library.alternate_views import auto_height, CM_TO_INCH
from calibre.gui2.widgets2 import Dialog
class BusyCursor(object):
@ -36,6 +41,115 @@ class BusyCursor(object):
def __exit__(self, *args):
QApplication.restoreOverrideCursor()
# IdLinksEditor {{{
class IdLinksRuleEdit(Dialog):
def __init__(self, key='', name='', template='', parent=None):
title = _('Edit rule') if key else _('Create a new rule')
Dialog.__init__(self, title=title, name='id-links-rule-editor', parent=parent)
self.key.setText(key), self.nw.setText(name), self.template.setText(template or 'http://example.com/{id}')
@property
def rule(self):
return self.key.text().lower(), self.nw.text(), self.template.text()
def setup_ui(self):
self.l = l = QFormLayout(self)
l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow)
l.addRow(QLabel(_(
'The key of the identifier, for example, in isbn:XXX, they key is isbn')))
self.key = k = QLineEdit(self)
l.addRow(_('&Key:'), k)
l.addRow(QLabel(_(
'The name that will appear in the book details panel')))
self.nw = n = QLineEdit(self)
l.addRow(_('&Name:'), n)
la = QLabel(_(
'The template used to create the link. The placeholder {id} in the template will be replaced with the actual identifier value.'))
la.setWordWrap(True)
l.addRow(la)
self.template = t = QLineEdit(self)
l.addRow(_('&Template:'), t)
t.selectAll()
t.setFocus(Qt.OtherFocusReason)
l.addWidget(self.bb)
def accept(self):
r = self.rule
for i, which in enumerate([_('Key'), _('Name'), _('Template')]):
if not r[i]:
return error_dialog(self, _('Value needed'), _(
'The %s field cannot be empty') % which, show=True)
Dialog.accept(self)
class IdLinksEditor(Dialog):
def __init__(self, parent=None):
Dialog.__init__(self, title=_('Create rules for identifiers'), name='id-links-rules-editor', parent=parent)
def setup_ui(self):
self.l = l = QVBoxLayout(self)
self.la = la = QLabel(_(
'Create rules to convert identifiers into links.'))
la.setWordWrap(True)
l.addWidget(la)
items = []
for k, lx in msprefs['id_link_rules'].iteritems():
for n, t in lx:
items.append((k, n, t))
items.sort(key=lambda x:sort_key(x[1]))
self.table = t = QTableWidget(len(items), 3, self)
t.setHorizontalHeaderLabels([_('Key'), _('Name'), _('Template')])
for r, (key, val, template) in enumerate(items):
t.setItem(r, 0, QTableWidgetItem(key))
t.setItem(r, 1, QTableWidgetItem(val))
t.setItem(r, 2, QTableWidgetItem(template))
l.addWidget(t)
t.horizontalHeader().setSectionResizeMode(2, t.horizontalHeader().Stretch)
self.cb = b = QPushButton(QIcon(I('plus.png')), _('&Add rule'), self)
b.clicked.connect(lambda : self.edit_rule())
self.bb.addButton(b, self.bb.ActionRole)
self.rb = b = QPushButton(QIcon(I('minus.png')), _('&Remove rule'), self)
b.clicked.connect(lambda : self.remove_rule())
self.bb.addButton(b, self.bb.ActionRole)
self.eb = b = QPushButton(QIcon(I('modified.png')), _('&Edit rule'), self)
b.clicked.connect(lambda : self.edit_rule(self.table.currentRow()))
self.bb.addButton(b, self.bb.ActionRole)
l.addWidget(self.bb)
def sizeHint(self):
return QSize(700, 550)
def accept(self):
rules = defaultdict(list)
for r in range(self.table.rowCount()):
def item(c):
return self.table.item(r, c).text()
rules[item(0)].append([item(1), item(2)])
msprefs['id_link_rules'] = dict(rules)
Dialog.accept(self)
def edit_rule(self, r=-1):
key = name = template = ''
if r > -1:
key, name, template = map(lambda c: self.table.item(r, c).text(), range(3))
d = IdLinksRuleEdit(key, name, template, self)
if d.exec_() == d.Accepted:
if r < 0:
self.table.setRowCount(self.table.rowCount() + 1)
r = self.table.rowCount() - 1
rule = d.rule
for c in range(3):
self.table.setItem(r, c, QTableWidgetItem(rule[c]))
self.table.scrollToItem(self.table.item(r, 0))
def remove_rule(self):
r = self.table.currentRow()
if r > -1:
self.table.removeRow(r)
# }}}
class DisplayedFields(QAbstractListModel): # {{{
def __init__(self, db, parent=None):
@ -178,6 +292,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Left'), 'left'), (_('Top'), 'top'), (_('Right'), 'right'), (_('Bottom'), 'bottom')])
r('book_list_extra_row_spacing', gprefs)
self.cover_browser_title_template_button.clicked.connect(self.edit_cb_title_template)
self.id_links_button.clicked.connect(self.edit_id_link_rules)
def get_esc_lang(l):
if l == 'en':
@ -310,6 +425,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.changed_signal.emit()
def edit_id_link_rules(self):
if IdLinksEditor(self).exec_() == Dialog.Accepted:
self.changed_signal.emit()
@property
def current_cover_size(self):
cval = self.opt_cover_grid_height.value()
@ -519,5 +638,3 @@ if __name__ == '__main__':
from calibre.gui2 import Application
app = Application([])
test_widget('Interface', 'Look & Feel')

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>820</width>
<width>843</width>
<height>546</height>
</rect>
</property>
@ -661,7 +661,7 @@ A value of zero means calculate automatically.</string>
<string>Book Details</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_12">
<item row="2" column="1">
<item row="3" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Note that &lt;b&gt;comments&lt;/b&gt; will always be displayed at the end, regardless of the position you assign here.</string>
@ -671,7 +671,7 @@ A value of zero means calculate automatically.</string>
</property>
</widget>
</item>
<item row="2" column="0" rowspan="2">
<item row="3" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select displayed metadata</string>
@ -801,6 +801,13 @@ Manage Authors. You can use the values {author} and
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="id_links_button">
<property name="text">
<string>Create rules to convert &amp;identifiers into links</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
@ -900,15 +907,15 @@ then the tags will be displayed each on their own line.</string>
</item>
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="opt_tag_browser_hide_empty_categories">
<property name="text">
<string>Hi&amp;de empty categories (columns) in the tag browser</string>
</property>
<property name="toolTip">
<string>When checked, calibre will automatically hide any category
(a column, custom or standard) that has no items to show. For example, some
categories might not have values when using virtual libraries. Checking this
box will cause these empty categories to be hidden.</string>
</property>
<property name="text">
<string>Hi&amp;de empty categories (columns) in the tag browser</string>
</property>
</widget>
</item>
<item row="1" column="1">
@ -956,7 +963,7 @@ if you never want subcategories</string>
<item row="5" column="1">
<widget class="QLineEdit" name="opt_cover_browser_title_template">
<property name="toolTip">
<string>The template used to generate the text below the covers. Uses the same syntax as save templates. Defaults to just the book title. Note that this setting is per-library, which means that you have to set it again for every different calibre library you use.</string>
<string>The template used to generate the text below the covers. Uses the same syntax as save templates. Defaults to just the book title. Note that this setting is per-library, which means that you have to set it again for every different calibre library you use.</string>
</property>
</widget>
</item>