From d4b85d69cca96d87a3a7a5c36ca2c9e755130fbf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Jul 2018 10:36:15 +0530 Subject: [PATCH] Preferences UI for author mapping rules --- src/calibre/ebooks/metadata/author_mapper.py | 5 +- src/calibre/gui2/author_mapper.py | 138 +++++++++++++++++++ src/calibre/gui2/preferences/adding.py | 20 ++- src/calibre/gui2/preferences/adding.ui | 29 +++- 4 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 src/calibre/gui2/author_mapper.py diff --git a/src/calibre/ebooks/metadata/author_mapper.py b/src/calibre/ebooks/metadata/author_mapper.py index a71a86af37..efb46d586a 100644 --- a/src/calibre/ebooks/metadata/author_mapper.py +++ b/src/calibre/ebooks/metadata/author_mapper.py @@ -99,12 +99,15 @@ def uniq(vals, kmap=icu_lower): return list(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) +def compile_rules(rules): + return tuple((r, matcher(r)) for r in rules) + + def map_authors(authors, rules=()): if not authors: return [] if not rules: return list(authors) - rules = [(r, matcher(r)) for r in rules] ans = [] for a in authors: ans.extend(apply_rules(a, rules)) diff --git a/src/calibre/gui2/author_mapper.py b/src/calibre/gui2/author_mapper.py new file mode 100644 index 0000000000..c38ae4bb80 --- /dev/null +++ b/src/calibre/gui2/author_mapper.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +from collections import OrderedDict + +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.ebooks.metadata.author_mapper import compile_rules, map_authors +from calibre.gui2 import Application, elided_text +from calibre.gui2.tag_mapper import ( + RuleEdit as RuleEditBase, RuleEditDialog as RuleEditDialogBase, + RuleItem as RuleItemBase, Rules as RulesBase, RulesDialog as RulesDialogBase, + Tester as TesterBase +) +from calibre.utils.config import JSONConfig + +author_maps = JSONConfig('author-mapping-rules') + + +class RuleEdit(RuleEditBase): + + ACTION_MAP = OrderedDict(( + ('replace', _('Change')), + ('capitalize', _('Capitalize')), + ('lower', _('Lower-case')), + ('upper', _('Upper-case')), + )) + + MATCH_TYPE_MAP = OrderedDict(( + ('one_of', _('is one of')), + ('not_one_of', _('is not one of')), + ('has', _('contains')), + ('matches', _('matches regex pattern')), + ('not_matches', _('does not match regex pattern')), + )) + + MSG = _('Create the rule below, the rule can be used to add or ignore files') + SUBJECT = _('the author, if the author name') + VALUE_ERROR = _('You must provide a value for the author name to match') + REPLACE_TEXT = _('with the name:') + + @property + def can_use_tag_editor(self): + return False + + def update_state(self): + a = self.action.currentData() + replace = a == 'replace' + self.la3.setVisible(replace), self.replace.setVisible(replace) + m = self.match_type.currentData() + is_match = 'matches' in m + self.regex_help.setVisible(is_match) + + @property + def rule(self): + return { + 'action': self.action.currentData(), + 'match_type': self.match_type.currentData(), + 'query': self.query.text().strip(), + 'replace': self.replace.text().strip(), + } + + @rule.setter + def rule(self, rule): + def sc(name): + c = getattr(self, name) + idx = c.findData(unicode(rule.get(name, ''))) + if idx < 0: + idx = 0 + c.setCurrentIndex(idx) + sc('match_type'), sc('action') + self.query.setText(unicode(rule.get('query', '')).strip()) + self.replace.setText(unicode(rule.get('replace', '')).strip()) + + +class RuleEditDialog(RuleEditDialogBase): + + PREFS_NAME = 'edit-author-mapping-rule' + RuleEditClass = RuleEdit + + +class RuleItem(RuleItemBase): + + @staticmethod + def text_from_rule(rule, parent): + query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') + text = _( + '{action} the author name, if it {match_type}: {query}').format( + action=RuleEdit.ACTION_MAP[rule['action']], match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query) + if rule['action'] == 'replace': + text += '
' + _('to the name') + ' %s' % rule['replace'] + return '
' + text + '
' + + +class Rules(RulesBase): + + RuleItemClass = RuleItem + RuleEditDialogClass = RuleEditDialog + MSG = _('You can specify rules to manipulate author names here.' + ' Click the "Add Rule" button' + ' below to get started. The rules will be processed in order for every author.') + + +class Tester(TesterBase): + + DIALOG_TITLE = _('Test author mapping rules') + PREFS_NAME = 'test-author-mapping-rules' + LABEL = _('Enter an author name to test:') + PLACEHOLDER = _('Enter author and click the "Test" button') + EMPTY_RESULT = '

 

' + + def do_test(self): + authors = string_to_authors(self.value.strip()) + ans = map_authors(authors, compile_rules(self.rules)) + self.result.setText(authors_to_string(ans)) + + +class RulesDialog(RulesDialogBase): + + DIALOG_TITLE = _('Edit author mapping rules') + PREFS_NAME = 'edit-author-mapping-rules' + RulesClass = Rules + TesterClass = Tester + PREFS_OBJECT = author_maps + + +if __name__ == '__main__': + app = Application([]) + d = RulesDialog() + d.rules = [ + {'action':'replace', 'query':'alice B & alice bob', 'match_type':'one_of', 'replace':'Alice Bob'}, + ] + d.exec_() + from pprint import pprint + pprint(d.rules) + del d, app diff --git a/src/calibre/gui2/preferences/adding.py b/src/calibre/gui2/preferences/adding.py index 05a4e1f13e..2bdc2c1b5d 100644 --- a/src/calibre/gui2/preferences/adding.py +++ b/src/calibre/gui2/preferences/adding.py @@ -52,8 +52,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'): signal = getattr(self.opt_blocked_auto_formats, 'item'+signal) signal.connect(self.blocked_auto_formats_changed) - self.tag_map_rules = self.add_filter_rules = None + self.tag_map_rules = self.add_filter_rules = self.author_map_rules = None self.tag_map_rules_button.clicked.connect(self.change_tag_map_rules) + self.author_map_rules_button.clicked.connect(self.change_author_map_rules) self.add_filter_rules_button.clicked.connect(self.change_add_filter_rules) self.tabWidget.setCurrentIndex(0) self.actions_tab.layout().setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) @@ -67,6 +68,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.tag_map_rules = d.rules self.changed_signal.emit() + def change_author_map_rules(self): + from calibre.gui2.author_mapper import RulesDialog + d = RulesDialog(self) + if gprefs.get('author_map_on_add_rules'): + d.rules = gprefs['author_map_on_add_rules'] + if d.exec_() == d.Accepted: + self.author_map_rules = d.rules + self.changed_signal.emit() + def change_add_filter_rules(self): from calibre.gui2.add_filters import RulesDialog d = RulesDialog(self) @@ -89,7 +99,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.filename_pattern.blockSignals(False) self.init_blocked_auto_formats() self.opt_automerge.setEnabled(self.opt_add_formats_to_existing.isChecked()) - self.tag_map_rules = self.add_filter_rules = None + self.tag_map_rules = self.add_filter_rules = self.author_map_rules = None # Blocked auto formats {{{ def blocked_auto_formats_changed(self, *args): @@ -130,6 +140,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.filename_pattern.initialize(defaults=True) self.init_blocked_auto_formats(defaults=True) self.tag_map_rules = [] + self.author_map_rules = [] self.add_filter_rules = [] def commit(self): @@ -171,6 +182,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): gprefs['tag_map_on_add_rules'] = self.tag_map_rules else: gprefs.pop('tag_map_on_add_rules', None) + if self.author_map_rules is not None: + if self.author_map_rules: + gprefs['author_map_on_add_rules'] = self.author_map_rules + else: + gprefs.pop('author_map_on_add_rules', None) if self.add_filter_rules is not None: if self.add_filter_rules: gprefs['add_filter_rules'] = self.add_filter_rules diff --git a/src/calibre/gui2/preferences/adding.ui b/src/calibre/gui2/preferences/adding.ui index 5eb2ccfbfe..29bc02ce8f 100644 --- a/src/calibre/gui2/preferences/adding.ui +++ b/src/calibre/gui2/preferences/adding.ui @@ -220,10 +220,17 @@ Author matching is exact. + + + + Qt::Horizontal + + + - Control how tags are processed: + Contro&l how tags are processed: tag_map_rules_button @@ -250,7 +257,7 @@ Author matching is exact. - Control which files are added during bulk imports: + Control which files are added during bul&k imports: add_filter_rules_button @@ -267,10 +274,20 @@ Author matching is exact. - - - - Qt::Horizontal + + + + Control how auth&ors are processed: + + + author_map_rules_button + + + + + + + Rules to manipulate a&uthor names