Preferences UI for author mapping rules

This commit is contained in:
Kovid Goyal 2018-07-18 10:36:15 +05:30
parent 626d5aa1ce
commit d4b85d69cc
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 183 additions and 9 deletions

View File

@ -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)) 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=()): def map_authors(authors, rules=()):
if not authors: if not authors:
return [] return []
if not rules: if not rules:
return list(authors) return list(authors)
rules = [(r, matcher(r)) for r in rules]
ans = [] ans = []
for a in authors: for a in authors:
ans.extend(apply_rules(a, rules)) ans.extend(apply_rules(a, rules))

View File

@ -0,0 +1,138 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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 = _(
'<b>{action}</b> the author name, if it <i>{match_type}</i>: <b>{query}</b>').format(
action=RuleEdit.ACTION_MAP[rule['action']], match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query)
if rule['action'] == 'replace':
text += '<br>' + _('to the name') + ' <b>%s</b>' % rule['replace']
return '<div style="white-space: nowrap">' + text + '</div>'
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 = '<p>&nbsp;</p>'
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

View File

@ -52,8 +52,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'): for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_blocked_auto_formats, 'item'+signal) signal = getattr(self.opt_blocked_auto_formats, 'item'+signal)
signal.connect(self.blocked_auto_formats_changed) 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.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.add_filter_rules_button.clicked.connect(self.change_add_filter_rules)
self.tabWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0)
self.actions_tab.layout().setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) self.actions_tab.layout().setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@ -67,6 +68,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.tag_map_rules = d.rules self.tag_map_rules = d.rules
self.changed_signal.emit() 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): def change_add_filter_rules(self):
from calibre.gui2.add_filters import RulesDialog from calibre.gui2.add_filters import RulesDialog
d = RulesDialog(self) d = RulesDialog(self)
@ -89,7 +99,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.filename_pattern.blockSignals(False) self.filename_pattern.blockSignals(False)
self.init_blocked_auto_formats() self.init_blocked_auto_formats()
self.opt_automerge.setEnabled(self.opt_add_formats_to_existing.isChecked()) 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 {{{ # Blocked auto formats {{{
def blocked_auto_formats_changed(self, *args): def blocked_auto_formats_changed(self, *args):
@ -130,6 +140,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.filename_pattern.initialize(defaults=True) self.filename_pattern.initialize(defaults=True)
self.init_blocked_auto_formats(defaults=True) self.init_blocked_auto_formats(defaults=True)
self.tag_map_rules = [] self.tag_map_rules = []
self.author_map_rules = []
self.add_filter_rules = [] self.add_filter_rules = []
def commit(self): def commit(self):
@ -171,6 +182,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
gprefs['tag_map_on_add_rules'] = self.tag_map_rules gprefs['tag_map_on_add_rules'] = self.tag_map_rules
else: else:
gprefs.pop('tag_map_on_add_rules', None) 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 is not None:
if self.add_filter_rules: if self.add_filter_rules:
gprefs['add_filter_rules'] = self.add_filter_rules gprefs['add_filter_rules'] = self.add_filter_rules

View File

@ -220,10 +220,17 @@ Author matching is exact.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="0" colspan="2">
<widget class="Line" name="line_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="14" column="0"> <item row="14" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Control how tags are processed:</string> <string>Contro&amp;l how tags are processed:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>tag_map_rules_button</cstring> <cstring>tag_map_rules_button</cstring>
@ -250,7 +257,7 @@ Author matching is exact.</string>
<item row="18" column="0"> <item row="18" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>Control which files are added during bulk imports:</string> <string>Control which files are added during bul&amp;k imports:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>add_filter_rules_button</cstring> <cstring>add_filter_rules_button</cstring>
@ -267,10 +274,20 @@ Author matching is exact.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="0" colspan="2"> <item row="15" column="0">
<widget class="Line" name="line_8"> <widget class="QLabel" name="label_7">
<property name="orientation"> <property name="text">
<enum>Qt::Horizontal</enum> <string>Control how auth&amp;ors are processed:</string>
</property>
<property name="buddy">
<cstring>author_map_rules_button</cstring>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QPushButton" name="author_map_rules_button">
<property name="text">
<string>Rules to manipulate a&amp;uthor names</string>
</property> </property>
</widget> </widget>
</item> </item>