Skip to content

Commit

Permalink
Bulk metadata edit: Add a new tab where you can create rules to trans…
Browse files Browse the repository at this point in the history
…form tags/authors/publishers for the selected books. Fixes #2064674 [[Enhancement] - Request method to bulk transform tags of selected ebooks using a preset list of rules](https://bugs.launchpad.net/calibre/+bug/2064674)
  • Loading branch information
kovidgoyal committed May 5, 2024
1 parent 5bc894d commit 6d9d698
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 8 deletions.
97 changes: 94 additions & 3 deletions src/calibre/gui2/dialogs/metadata_bulk.py
Expand Up @@ -52,7 +52,9 @@
'remove_all remove add au aus do_aus rating pub do_series do_autonumber '
'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')
'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'
)

null = object()

Expand Down Expand Up @@ -102,6 +104,12 @@ def __init__(self, args, ids, db, refresh_books, cc_widgets, s_r_func, do_sr, sr
bool(do_sr), args.do_compress_cover
]
self.selected_options = sum(options)
if args.tag_map_rules:
self.selected_options += 1
if args.author_map_rules:
self.selected_options += 1
if args.publisher_map_rules:
self.selected_options += 1
if DEBUG:
print("Number of steps for bulk metadata: %d" % self.selected_options)
print("Optionslist: ")
Expand Down Expand Up @@ -238,6 +246,9 @@ def do_all(self):
cache = self.db.new_api
args = self.args
from_file = args.cover_action == 'fromfmt' or args.read_file_metadata
if args.author_map_rules:
from calibre.ebooks.metadata.author_mapper import compile_rules
args = args._replace(author_map_rules=compile_rules(args.author_map_rules))
if from_file:
old = prefs['read_file_metadata']
if not old:
Expand Down Expand Up @@ -315,6 +326,20 @@ def get_sort(book_id):
cache.set_field('author_sort', {bid:args.aus for bid in self.ids})
self.progress_finished_cur_step.emit()

if args.author_map_rules:
self.progress_next_step_range.emit(0)
from calibre.ebooks.metadata.author_mapper import map_authors
authors_map = cache.all_field_for('authors', self.ids)
changed, sorts = {}, {}
for book_id, authors in authors_map.items():
new_authors = map_authors(authors, args.author_map_rules)
if tuple(new_authors) != tuple(authors):
changed[book_id] = new_authors
sorts[book_id] = cache.author_sort_from_authors(new_authors)
cache.set_field('authors', changed)
cache.set_field('author_sort', sorts)
self.progress_finished_cur_step.emit()

# Covers
if args.cover_action == 'remove':
self.progress_next_step_range.emit(0)
Expand Down Expand Up @@ -386,6 +411,19 @@ def get_sort(book_id):
cache.set_field('publisher', {bid: args.pub for bid in self.ids})
self.progress_finished_cur_step.emit()

if args.publisher_map_rules:
self.progress_next_step_range.emit(0)
from calibre.ebooks.metadata.tag_mapper import map_tags
publishers_map = cache.all_field_for('publisher', self.ids)
changed = {}
for book_id, publisher in publishers_map.items():
new_publishers = map_tags([publisher], args.publisher_map_rules)
new_publisher = new_publishers[0] if new_publishers else ''
if new_publisher != publisher:
changed[book_id] = new_publisher
cache.set_field('publisher', 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})
Expand Down Expand Up @@ -458,6 +496,18 @@ def next_series_num(bid, i):
self.db.bulk_modify_tags(self.ids, add=args.add, remove=args.remove)
self.progress_finished_cur_step.emit()

if args.tag_map_rules:
self.progress_next_step_range.emit(0)
from calibre.ebooks.metadata.tag_mapper import map_tags
tags_map = cache.all_field_for('tags', self.ids)
changed = {}
for book_id, tags in tags_map.items():
new_tags = map_tags(tags, args.tag_map_rules)
if new_tags != tags:
changed[book_id] = new_tags
cache.set_field('tags', changed)
self.progress_finished_cur_step.emit()

if args.do_compress_cover:
self.progress_next_step_range.emit(len(self.ids))

Expand Down Expand Up @@ -582,7 +632,6 @@ def __init__(self, window, rows, model, tab, refresh_books):
'Immediately make all changes without closing the dialog. '
'This operation cannot be canceled or undone'))
self.do_again = False
self.central_widget.setCurrentIndex(tab)
self.restore_geometry(gprefs, 'bulk_metadata_window_geometry')
ct = gprefs.get('bulk_metadata_window_tab', 0)
self.central_widget.setCurrentIndex(ct)
Expand All @@ -591,8 +640,47 @@ def __init__(self, window, rows, model, tab, refresh_books):
self.authors.setFocus(Qt.FocusReason.OtherFocusReason)
self.generate_cover_settings = None
self.button_config_cover_gen.clicked.connect(self.customize_cover_generation)
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.update_transform_labels()
self.central_widget.setCurrentIndex(tab)
self.exec()

def update_transform_labels(self):
def f(label, count):
if count:
label.setText(_('Number of rules: {}').format(count))
else:
label.setText(_('There are currently no rules'))
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))

def _change_transform_rules(self, RulesDialog, which):
d = RulesDialog(self)
pref = f'{which}_map_on_bulk_metadata_rules'
previously_used = gprefs.get(pref)
if previously_used:
d.rules = previously_used
if d.exec() == QDialog.DialogCode.Accepted:
setattr(self, f'{which}_map_rules', d.rules)
gprefs.set(pref, d.rules)
self.update_transform_labels()

def transform_tags(self):
from calibre.gui2.tag_mapper import RulesDialog
self._change_transform_rules(RulesDialog, 'tag')

def transform_authors(self):
from calibre.gui2.author_mapper import RulesDialog
self._change_transform_rules(RulesDialog, 'author')

def transform_publishers(self):
from calibre.gui2.publisher_mapper import RulesDialog
self._change_transform_rules(RulesDialog, 'publisher')

def sizeHint(self):
geom = self.screen().availableSize()
nh, nw = max(300, geom.height()-50), max(400, geom.width()-70)
Expand Down Expand Up @@ -1254,7 +1342,10 @@ def accept(self):
do_title_case, cover_action, clear_series, clear_pub, pubdate,
adddate, do_title_sort, languages, clear_languages,
restore_original, self.comments, self.generate_cover_settings,
read_file_metadata, self.casing_map[self.casing_algorithm.currentIndex()], do_compress_cover, compress_cover_quality)
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
)
if DEBUG:
print('Running bulk metadata operation with settings:')
print(args)
Expand Down
124 changes: 119 additions & 5 deletions src/calibre/gui2/dialogs/metadata_bulk.ui
Expand Up @@ -39,7 +39,7 @@
<x>0</x>
<y>0</y>
<width>933</width>
<height>660</height>
<height>658</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
Expand Down Expand Up @@ -783,8 +783,8 @@ as that of the first selected book.</string>
<rect>
<x>0</x>
<y>0</y>
<width>804</width>
<height>388</height>
<width>729</width>
<height>429</height>
</rect>
</property>
<layout class="QGridLayout" name="vargrid">
Expand Down Expand Up @@ -1264,8 +1264,8 @@ not multiple and the destination field is multiple</string>
<rect>
<x>0</x>
<y>0</y>
<width>203</width>
<height>70</height>
<width>187</width>
<height>72</height>
</rect>
</property>
<layout class="QGridLayout" name="testgrid">
Expand Down Expand Up @@ -1319,6 +1319,120 @@ not multiple and the destination field is multiple</string>
</layout>
</widget>
</widget>
<widget class="QWidget" name="transform_tab">
<attribute name="title">
<string>&amp;Transform tags/authors</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Create rules to transform metadata by clicking the buttons below. The rules will be applied to all selected books when you click OK.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="button_transform_tags">
<property name="text">
<string>Transform &amp;tags</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_transform_tags">
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<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>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QPushButton" name="button_transform_authors">
<property name="text">
<string>Transform &amp;authors</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_transform_authors">
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<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>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QPushButton" name="button_transform_publishers">
<property name="text">
<string>Transform &amp;publishers</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_transform_publishers">
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<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">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
Expand Down

2 comments on commit 6d9d698

@cbhaley
Copy link
Contributor

@cbhaley cbhaley commented on 6d9d698 May 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be an explicit "enable rules" box for each transformation that is set to unchecked by default. Reason: as it is, if I use bulk edit to do something else, for example an S&R or modify a custom column, then the transforms will be applied "invisibly". As far as I can tell there is no way to prevent this beyond deleting the rules. In addition, in some cases I might want to transform authors but not tags or publishers.

@kovidgoyal
Copy link
Owner Author

@kovidgoyal kovidgoyal commented on 6d9d698 May 5, 2024 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.