diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py
index 144ebef76a..16b6a1b605 100644
--- a/src/calibre/ebooks/metadata/sources/identify.py
+++ b/src/calibre/ebooks/metadata/sources/identify.py
@@ -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):
diff --git a/src/calibre/ebooks/metadata/sources/prefs.py b/src/calibre/ebooks/metadata/sources/prefs.py
index b5496c1797..0abdb69f66 100644
--- a/src/calibre/ebooks/metadata/sources/prefs.py
+++ b/src/calibre/ebooks/metadata/sources/prefs.py
@@ -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
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 3317d966dc..23ef221987 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -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: %s') % 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')
-
-
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index f4abcc1476..d7dc013629 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -6,7 +6,7 @@
0
0
- 820
+ 843
546
@@ -661,7 +661,7 @@ A value of zero means calculate automatically.
Book Details
- -
+
-
Note that <b>comments</b> will always be displayed at the end, regardless of the position you assign here.
@@ -671,7 +671,7 @@ A value of zero means calculate automatically.
- -
+
-
Select displayed metadata
@@ -801,6 +801,13 @@ Manage Authors. You can use the values {author} and
+ -
+
+
+ Create rules to convert &identifiers into links
+
+
+
@@ -900,15 +907,15 @@ then the tags will be displayed each on their own line.
-
-
- Hi&de empty categories (columns) in the tag browser
-
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.
+
+ Hi&de empty categories (columns) in the tag browser
+
-
@@ -956,7 +963,7 @@ if you never want subcategories
-
- 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.
+ 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.