mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Allow creating rules to transform series names in the Bulk metadata editor and Preferences->Metadata download. Fixes #2091268 [[Enhancement] Series Mapper](https://bugs.launchpad.net/calibre/+bug/2091268)
This commit is contained in:
parent
94395c3e3c
commit
925bd06a1b
@ -502,7 +502,8 @@ def identify(log, abort, # {{{
|
||||
(len(results), time.time() - start_time))
|
||||
tm_rules = msprefs['tag_map_rules']
|
||||
pm_rules = msprefs['publisher_map_rules']
|
||||
if tm_rules or pm_rules:
|
||||
s_rules = msprefs['series_map_rules']
|
||||
if tm_rules or pm_rules or s_rules:
|
||||
from calibre.ebooks.metadata.tag_mapper import map_tags
|
||||
am_rules = msprefs['author_map_rules']
|
||||
if am_rules:
|
||||
@ -535,6 +536,9 @@ def identify(log, abort, # {{{
|
||||
if pm_rules and r.publisher:
|
||||
pubs = map_tags([r.publisher], pm_rules)
|
||||
r.publisher = pubs[0] if pubs else ''
|
||||
if s_rules and r.series:
|
||||
ss = map_tags([r.series], s_rules)
|
||||
r.series = ss[0] if ss else ''
|
||||
|
||||
if msprefs['swap_author_names']:
|
||||
for r in results:
|
||||
|
@ -21,6 +21,7 @@ msprefs.defaults['append_comments'] = False
|
||||
msprefs.defaults['tag_map_rules'] = ()
|
||||
msprefs.defaults['author_map_rules'] = ()
|
||||
msprefs.defaults['publisher_map_rules'] = ()
|
||||
msprefs.defaults['series_map_rules'] = ()
|
||||
msprefs.defaults['id_link_rules'] = {}
|
||||
msprefs.defaults['keep_dups'] = False
|
||||
|
||||
|
@ -54,7 +54,7 @@ Settings = namedtuple('Settings',
|
||||
'do_swap_ta do_remove_conv do_auto_author series do_series_restart series_start_value series_increment '
|
||||
'do_title_case cover_action clear_series clear_pub pubdate adddate do_title_sort languages clear_languages '
|
||||
'restore_original comments generate_cover_settings read_file_metadata casing_algorithm do_compress_cover compress_cover_quality '
|
||||
'tag_map_rules author_map_rules publisher_map_rules'
|
||||
'tag_map_rules author_map_rules publisher_map_rules series_map_rules'
|
||||
)
|
||||
|
||||
null = object()
|
||||
@ -111,6 +111,8 @@ class MyBlockingBusy(QDialog): # {{{
|
||||
self.selected_options += 1
|
||||
if args.publisher_map_rules:
|
||||
self.selected_options += 1
|
||||
if args.series_map_rules:
|
||||
self.selected_options += 1
|
||||
if DEBUG:
|
||||
print("Number of steps for bulk metadata: %d" % self.selected_options)
|
||||
print("Optionslist: ")
|
||||
@ -425,6 +427,19 @@ class MyBlockingBusy(QDialog): # {{{
|
||||
cache.set_field('publisher', changed)
|
||||
self.progress_finished_cur_step.emit()
|
||||
|
||||
if args.series_map_rules:
|
||||
self.progress_next_step_range.emit(0)
|
||||
from calibre.ebooks.metadata.tag_mapper import map_tags
|
||||
series_map = cache.all_field_for('series', self.ids)
|
||||
changed = {}
|
||||
for book_id, series in series_map.items():
|
||||
new_series = map_tags([series], args.series_map_rules)
|
||||
new_series = new_series[0] if new_series else ''
|
||||
if new_series != series:
|
||||
changed[book_id] = new_series
|
||||
cache.set_field('series', changed)
|
||||
self.progress_finished_cur_step.emit()
|
||||
|
||||
if args.clear_series:
|
||||
self.progress_next_step_range.emit(0)
|
||||
cache.set_field('series', {bid: '' for bid in self.ids})
|
||||
@ -643,7 +658,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
self.button_transform_tags.clicked.connect(self.transform_tags)
|
||||
self.button_transform_authors.clicked.connect(self.transform_authors)
|
||||
self.button_transform_publishers.clicked.connect(self.transform_publishers)
|
||||
self.tag_map_rules = self.author_map_rules = self.publisher_map_rules = ()
|
||||
self.button_transform_series.clicked.connect(self.transform_series)
|
||||
self.tag_map_rules = self.author_map_rules = self.publisher_map_rules = self.series_map_rules = ()
|
||||
tuple(map(lambda b: (b.clicked.connect(self.clear_transform_rules_for), b.setIcon(QIcon.ic('clear_left.png')), b.setToolTip(_(
|
||||
'Clear the rules'))),
|
||||
(self.button_clear_tags_rules, self.button_clear_authors_rules, self.button_clear_publishers_rules)
|
||||
@ -663,9 +679,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
f(self.label_transform_tags, len(self.tag_map_rules))
|
||||
f(self.label_transform_authors, len(self.author_map_rules))
|
||||
f(self.label_transform_publishers, len(self.publisher_map_rules))
|
||||
f(self.label_transform_series, len(self.series_map_rules))
|
||||
self.button_clear_tags_rules.setVisible(bool(self.tag_map_rules))
|
||||
self.button_clear_authors_rules.setVisible(bool(self.author_map_rules))
|
||||
self.button_clear_publishers_rules.setVisible(bool(self.publisher_map_rules))
|
||||
self.button_clear_series_rules.setVisible(bool(self.series_map_rules))
|
||||
|
||||
def clear_transform_rules_for(self):
|
||||
n = self.sender().objectName()
|
||||
@ -675,6 +693,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
self.author_map_rules = ()
|
||||
elif 'publisher' in n:
|
||||
self.publisher_map_rules = ()
|
||||
elif 'series' in n:
|
||||
self.series_map_rules = ()
|
||||
self.update_transform_labels()
|
||||
|
||||
def _change_transform_rules(self, RulesDialog, which):
|
||||
@ -700,6 +720,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
from calibre.gui2.publisher_mapper import RulesDialog
|
||||
self._change_transform_rules(RulesDialog, 'publisher')
|
||||
|
||||
def transform_series(self):
|
||||
from calibre.gui2.series_mapper import RulesDialog
|
||||
self._change_transform_rules(RulesDialog, 'series')
|
||||
|
||||
def sizeHint(self):
|
||||
geom = self.screen().availableSize()
|
||||
nh, nw = max(300, geom.height()-50), max(400, geom.width()-70)
|
||||
@ -1378,7 +1402,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
restore_original, self.comments, self.generate_cover_settings,
|
||||
read_file_metadata, self.casing_map[self.casing_algorithm.currentIndex()],
|
||||
do_compress_cover, compress_cover_quality, self.tag_map_rules, self.author_map_rules,
|
||||
self.publisher_map_rules
|
||||
self.publisher_map_rules, self.series_map_rules,
|
||||
)
|
||||
if DEBUG:
|
||||
print('Running bulk metadata operation with settings:')
|
||||
|
@ -783,7 +783,7 @@ as that of the first selected book.</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>729</width>
|
||||
<width>719</width>
|
||||
<height>429</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -1271,7 +1271,7 @@ not multiple and the destination field is multiple</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>187</width>
|
||||
<width>687</width>
|
||||
<height>72</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -1443,6 +1443,40 @@ not multiple and the destination field is multiple</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_transform_series">
|
||||
<property name="text">
|
||||
<string>Transform &series</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_transform_series"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="button_clear_series_rules">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_10">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
|
@ -342,11 +342,12 @@ 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 = self.publisher_map_rules = None
|
||||
self.tag_map_rules = self.author_map_rules = self.publisher_map_rules = self.series_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)
|
||||
m.addAction(_('Series')).triggered.connect(self.change_series_map_rules)
|
||||
self.map_rules_button.setMenu(m)
|
||||
l = self.page.layout()
|
||||
l.setStretch(0, 1)
|
||||
@ -401,6 +402,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.publisher_map_rules = d.rules
|
||||
self.changed_signal.emit()
|
||||
|
||||
def change_series_map_rules(self):
|
||||
from calibre.gui2.series_mapper import RulesDialog
|
||||
d = RulesDialog(self)
|
||||
if msprefs.get('series_map_rules'):
|
||||
d.rules = list(msprefs['series_map_rules'])
|
||||
if d.exec() == QDialog.DialogCode.Accepted:
|
||||
self.series_map_rules = d.rules
|
||||
self.changed_signal.emit()
|
||||
|
||||
def change_author_map_rules(self):
|
||||
from calibre.gui2.author_mapper import RulesDialog
|
||||
d = RulesDialog(self)
|
||||
|
138
src/calibre/gui2/series_mapper.py
Normal file
138
src/calibre/gui2/series_mapper.py
Normal file
@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
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
|
||||
from calibre.gui2.tag_mapper import RuleEditDialog as RuleEditDialogBase
|
||||
from calibre.gui2.tag_mapper import RuleItem as RuleItemBase
|
||||
from calibre.gui2.tag_mapper import Rules as RulesBase
|
||||
from calibre.gui2.tag_mapper import RulesDialog as RulesDialogBase
|
||||
from calibre.gui2.tag_mapper import Tester as TesterBase
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
||||
series_maps = JSONConfig('series-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 series')
|
||||
SUBJECT = _('the series, if the series name')
|
||||
VALUE_ERROR = _('You must provide a value for the series name to match')
|
||||
REPLACE_TEXT = _('with the name:')
|
||||
SINGLE_EDIT_FIELD_NAME = 'series'
|
||||
|
||||
@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-series-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 series 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 series names here.'
|
||||
' Click the "Add Rule" button'
|
||||
' below to get started. The rules will be processed in order for every series.')
|
||||
|
||||
|
||||
class Tester(TesterBase):
|
||||
|
||||
DIALOG_TITLE = _('Test series mapping rules')
|
||||
PREFS_NAME = 'test-series-mapping-rules'
|
||||
LABEL = _('Enter a series name to test:')
|
||||
PLACEHOLDER = _('Enter series and click the "Test" button')
|
||||
EMPTY_RESULT = '<p> </p>'
|
||||
|
||||
def do_test(self):
|
||||
series = self.value.strip()
|
||||
ans = map_tags([series], self.rules)
|
||||
self.result.setText((ans or ('',))[0])
|
||||
|
||||
|
||||
class RulesDialog(RulesDialogBase):
|
||||
|
||||
DIALOG_TITLE = _('Edit series mapping rules')
|
||||
PREFS_NAME = 'edit-series-mapping-rules'
|
||||
RulesClass = Rules
|
||||
TesterClass = Tester
|
||||
PREFS_OBJECT = series_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
|
Loading…
x
Reference in New Issue
Block a user