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.