mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Preferences UI for author mapping rules
This commit is contained in:
parent
626d5aa1ce
commit
d4b85d69cc
@ -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))
|
||||||
|
138
src/calibre/gui2/author_mapper.py
Normal file
138
src/calibre/gui2/author_mapper.py
Normal 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> </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
|
@ -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
|
||||||
|
@ -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&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&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&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&uthor names</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user