From b9063801844f117c2269333fd6834b54acb4f42b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Apr 2023 15:08:56 +0530 Subject: [PATCH] Metadata download: Allow specifying rules to transform publisher names in addition to author and tag names. Fixes #2012304 [[Enhancement] in "Metadata Download," add "rules to filter/transform publisher"](https://bugs.launchpad.net/calibre/+bug/2012304) --- .../ebooks/metadata/sources/identify.py | 6 +- src/calibre/ebooks/metadata/sources/prefs.py | 5 +- src/calibre/gui2/author_mapper.py | 2 +- .../gui2/preferences/metadata_sources.py | 20 ++- .../gui2/preferences/metadata_sources.ui | 2 +- src/calibre/gui2/publisher_mapper.py | 137 ++++++++++++++++++ 6 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 src/calibre/gui2/publisher_mapper.py diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 1e13ce63a4..d400b25dea 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -501,7 +501,8 @@ def identify(log, abort, # {{{ log('We have %d merged results, merging took: %.2f seconds' % (len(results), time.time() - start_time)) tm_rules = msprefs['tag_map_rules'] - if tm_rules: + pm_rules = msprefs['publisher_map_rules'] + if tm_rules or pm_rules: from calibre.ebooks.metadata.tag_mapper import map_tags am_rules = msprefs['author_map_rules'] if am_rules: @@ -531,6 +532,9 @@ def identify(log, abort, # {{{ r.tags = r.tags[:max_tags] if getattr(r.pubdate, 'year', 2000) <= UNDEFINED_DATE.year: r.pubdate = None + if pm_rules and r.publisher: + pubs = map_tags([r.publisher], pm_rules) + r.publisher = pubs[0] if pubs else '' if msprefs['swap_author_names']: for r in results: diff --git a/src/calibre/ebooks/metadata/sources/prefs.py b/src/calibre/ebooks/metadata/sources/prefs.py index 5725a87d8c..956494ef34 100644 --- a/src/calibre/ebooks/metadata/sources/prefs.py +++ b/src/calibre/ebooks/metadata/sources/prefs.py @@ -18,8 +18,9 @@ msprefs.defaults['swap_author_names'] = False msprefs.defaults['fewer_tags'] = True msprefs.defaults['find_first_edition_date'] = False msprefs.defaults['append_comments'] = False -msprefs.defaults['tag_map_rules'] = [] -msprefs.defaults['author_map_rules'] = [] +msprefs.defaults['tag_map_rules'] = () +msprefs.defaults['author_map_rules'] = () +msprefs.defaults['publisher_map_rules'] = () msprefs.defaults['id_link_rules'] = {} msprefs.defaults['keep_dups'] = False diff --git a/src/calibre/gui2/author_mapper.py b/src/calibre/gui2/author_mapper.py index 5d26d08a9c..3fe9395361 100644 --- a/src/calibre/gui2/author_mapper.py +++ b/src/calibre/gui2/author_mapper.py @@ -34,7 +34,7 @@ class RuleEdit(RuleEditBase): ('not_matches', _('does not match regex pattern')), )) - MSG = _('Create the rule below, the rule can be used to add or ignore files') + MSG = _('Create the rule below, the rule can be used to add or ignore authors') 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:') diff --git a/src/calibre/gui2/preferences/metadata_sources.py b/src/calibre/gui2/preferences/metadata_sources.py index 0ded5efd2d..cb9bfe4e97 100644 --- a/src/calibre/gui2/preferences/metadata_sources.py +++ b/src/calibre/gui2/preferences/metadata_sources.py @@ -332,10 +332,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.select_default_button.clicked.connect(self.fields_model.select_user_defaults) self.select_default_button.clicked.connect(self.changed_signal) self.set_as_default_button.clicked.connect(self.fields_model.commit_user_defaults) - self.tag_map_rules = self.author_map_rules = None + self.tag_map_rules = self.author_map_rules = self.publisher_map_rules = None m = QMenu(self) m.addAction(_('Tags')).triggered.connect(self.change_tag_map_rules) m.addAction(_('Authors')).triggered.connect(self.change_author_map_rules) + m.addAction(_('Publisher')).triggered.connect(self.change_publisher_map_rules) self.map_rules_button.setMenu(m) l = self.page.layout() l.setStretch(0, 1) @@ -376,16 +377,25 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): from calibre.gui2.tag_mapper import RulesDialog d = RulesDialog(self) if msprefs.get('tag_map_rules'): - d.rules = msprefs['tag_map_rules'] + d.rules = list(msprefs['tag_map_rules']) if d.exec() == QDialog.DialogCode.Accepted: self.tag_map_rules = d.rules self.changed_signal.emit() + def change_publisher_map_rules(self): + from calibre.gui2.publisher_mapper import RulesDialog + d = RulesDialog(self) + if msprefs.get('publisher_map_rules'): + d.rules = list(msprefs['publisher_map_rules']) + if d.exec() == QDialog.DialogCode.Accepted: + self.publisher_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 msprefs.get('author_map_rules'): - d.rules = msprefs['author_map_rules'] + d.rules = list(msprefs['author_map_rules']) if d.exec() == QDialog.DialogCode.Accepted: self.author_map_rules = d.rules self.changed_signal.emit() @@ -395,7 +405,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.sources_model.initialize() self.sources_view.resizeColumnsToContents() self.fields_model.initialize() - self.tag_map_rules = self.author_map_rules = None + self.tag_map_rules = self.author_map_rules = self.publisher_map_rules = None def restore_defaults(self): ConfigWidgetBase.restore_defaults(self) @@ -410,6 +420,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): msprefs['tag_map_rules'] = self.tag_map_rules or [] if self.author_map_rules is not None: msprefs['author_map_rules'] = self.author_map_rules or [] + if self.publisher_map_rules is not None: + msprefs['publisher_map_rules'] = self.publisher_map_rules or [] return ConfigWidgetBase.commit(self) diff --git a/src/calibre/gui2/preferences/metadata_sources.ui b/src/calibre/gui2/preferences/metadata_sources.ui index dfc98a8844..b978aacdb2 100644 --- a/src/calibre/gui2/preferences/metadata_sources.ui +++ b/src/calibre/gui2/preferences/metadata_sources.ui @@ -234,7 +234,7 @@ - Create &rules to transform tags/authors + Create &rules to transform tags/authors/publishers QToolButton::InstantPopup diff --git a/src/calibre/gui2/publisher_mapper.py b/src/calibre/gui2/publisher_mapper.py new file mode 100644 index 0000000000..d678dded0f --- /dev/null +++ b/src/calibre/gui2/publisher_mapper.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# License: GPL v3 Copyright: 2018, Kovid Goyal + + +from collections import OrderedDict + +from calibre.ebooks.metadata.tag_mapper import map_tags +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 + +publisher_maps = JSONConfig('publisher-mapping-rules') + + +class RuleEdit(RuleEditBase): + + ACTION_MAP = OrderedDict(( + ('replace', _('Change')), + ('capitalize', _('Capitalize')), + ('titlecase', _('Title-case')), + ('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 modify publishers') + SUBJECT = _('the publisher, if the publisher name') + VALUE_ERROR = _('You must provide a value for the publisher name to match') + REPLACE_TEXT = _('with the name:') + SINGLE_EDIT_FIELD_NAME = 'publisher' + + @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(str(rule.get(name, ''))) + if idx < 0: + idx = 0 + c.setCurrentIndex(idx) + sc('match_type'), sc('action') + self.query.setText(str(rule.get('query', '')).strip()) + self.replace.setText(str(rule.get('replace', '')).strip()) + + +class RuleEditDialog(RuleEditDialogBase): + + PREFS_NAME = 'edit-publisher-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 publisher 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 publisher names here.' + ' Click the "Add Rule" button' + ' below to get started. The rules will be processed in order for every publisher.') + + +class Tester(TesterBase): + + DIALOG_TITLE = _('Test publisher mapping rules') + PREFS_NAME = 'test-publisher-mapping-rules' + LABEL = _('Enter an publisher name to test:') + PLACEHOLDER = _('Enter publisher and click the "Test" button') + EMPTY_RESULT = '

 

' + + def do_test(self): + publisher = self.value.strip() + ans = map_tags([publisher], self.rules) + self.result.setText((ans or ('',))[0]) + + +class RulesDialog(RulesDialogBase): + + DIALOG_TITLE = _('Edit publisher mapping rules') + PREFS_NAME = 'edit-publisher-mapping-rules' + RulesClass = Rules + TesterClass = Tester + PREFS_OBJECT = publisher_maps + + +if __name__ == '__main__': + app = Application([]) + d = RulesDialog() + d.rules = [ + {'action':'replace', 'query':'alice Bob', 'match_type':'one_of', 'replace':'Alice Bob'}, + ] + d.exec() + from pprint import pprint + pprint(d.rules) + del d, app