diff --git a/src/calibre/ebooks/metadata/tag_mapper.py b/src/calibre/ebooks/metadata/tag_mapper.py new file mode 100644 index 0000000000..237848bb0f --- /dev/null +++ b/src/calibre/ebooks/metadata/tag_mapper.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2015, Kovid Goyal + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +import regex +REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.IGNORECASE | regex.UNICODE + + +def matcher(rule): + mt = rule['match_type'] + if mt == 'one_of': + tags = {icu_lower(x.strip()) for x in rule['query'].split(',')} + return lambda x: x in tags + + if mt == 'not_one_of': + tags = {icu_lower(x.strip()) for x in rule['query'].split(',')} + return lambda x: x not in tags + + if mt == 'matches': + pat = regex.compile(rule['query'], flags=REGEX_FLAGS) + return lambda x: pat.match(x) is not None + + if mt == 'not_matches': + pat = regex.compile(rule['query'], flags=REGEX_FLAGS) + return lambda x: pat.match(x) is None + + return lambda x: False + + +def apply_rules(tag, rules): + for rule, matches in rules: + ltag = icu_lower(tag) + if matches(ltag): + ac = rule['action'] + if ac == 'remove': + return None + if ac == 'keep': + return tag + if ac == 'replace': + tag = regex.sub(rule['query'], rule['replace'], flags=REGEX_FLAGS) + return tag + + +def map_tags(tags, rules=()): + if not tags: + return [] + if not rules: + return list(tags) + rules = [(r, matcher(r)) for r in rules] + return [x for x in (apply_rules(t, rules) for t in tags) if x] diff --git a/src/calibre/gui2/tag_mapper.py b/src/calibre/gui2/tag_mapper.py index cdb8cfe0c2..3e010ce63d 100644 --- a/src/calibre/gui2/tag_mapper.py +++ b/src/calibre/gui2/tag_mapper.py @@ -14,6 +14,7 @@ from PyQt5.Qt import ( QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu ) +from calibre.ebooks.metadata.tag_mapper import map_tags from calibre.gui2 import error_dialog, elided_text, Application, question_dialog from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig @@ -272,11 +273,43 @@ class Rules(QWidget): if 'action' in rule and 'match_type' in rule and 'query' in rule: RuleItem(rule, self.rule_list) +class Tester(Dialog): + + def __init__(self, rules, parent=None): + self.rules = rules + Dialog.__init__(self, _('Test tag mapper rules'), 'test-tag-mapper-rules', parent=parent) + + def setup_ui(self): + self.l = l = QVBoxLayout(self) + self.bb.setStandardButtons(self.bb.Close) + self.la = la = QLabel(_( + 'Enter a comma separated list of &tags to test:')) + l.addWidget(la) + self.tags = t = QLineEdit(self) + la.setBuddy(t) + t.setPlaceholderText(_('Enter tags and click the Test button')) + self.h = h = QHBoxLayout() + l.addLayout(h) + h.addWidget(t) + self.test_button = b = QPushButton(_('&Test'), self) + b.clicked.connect(self.do_test) + h.addWidget(b) + self.result = la = QLabel(self) + la.setWordWrap(True) + l.addWidget(la) + l.addWidget(self.bb) + + def do_test(self): + tags = [x.strip() for x in self.tags.text().split(',')] + tags = map_tags(tags, self.rules) + self.result.setText(_('Resulting tags: %s') % ', '.join(tags)) + + class RulesDialog(Dialog): def __init__(self, parent=None): self.loaded_ruleset = None - Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=None) + Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=parent) def setup_ui(self): self.l = l = QVBoxLayout(self) @@ -291,6 +324,8 @@ class RulesDialog(Dialog): self.load_menu = QMenu(self) b.setMenu(self.load_menu) self.build_load_menu() + self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole) + b.clicked.connect(self.test_rules) @property def rules(self): @@ -336,6 +371,9 @@ class RulesDialog(Dialog): del tag_maps[name] self.build_load_menu() + def test_rules(self): + Tester(self.rules, self).exec_() + if __name__ == '__main__': app = Application([]) d = RulesDialog()