From 6d9d698ab2e5e4ab704cef1ef3c95ab6607fee22 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 May 2024 12:54:47 +0530 Subject: [PATCH] Bulk metadata edit: Add a new tab where you can create rules to transform 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) --- src/calibre/gui2/dialogs/metadata_bulk.py | 97 ++++++++++++++++- src/calibre/gui2/dialogs/metadata_bulk.ui | 124 +++++++++++++++++++++- 2 files changed, 213 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 12e7021e05..f4eb5f50a0 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -52,7 +52,9 @@ Settings = namedtuple('Settings', '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() @@ -102,6 +104,12 @@ class MyBlockingBusy(QDialog): # {{{ 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: ") @@ -238,6 +246,9 @@ class MyBlockingBusy(QDialog): # {{{ 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: @@ -315,6 +326,20 @@ class MyBlockingBusy(QDialog): # {{{ 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) @@ -386,6 +411,19 @@ class MyBlockingBusy(QDialog): # {{{ 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}) @@ -458,6 +496,18 @@ class MyBlockingBusy(QDialog): # {{{ 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)) @@ -582,7 +632,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): '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) @@ -591,8 +640,47 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): 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) @@ -1254,7 +1342,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): 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) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 2166b751a9..7aff2dd06f 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -39,7 +39,7 @@ 0 0 933 - 660 + 658 @@ -783,8 +783,8 @@ as that of the first selected book. 0 0 - 804 - 388 + 729 + 429 @@ -1264,8 +1264,8 @@ not multiple and the destination field is multiple 0 0 - 203 - 70 + 187 + 72 @@ -1319,6 +1319,120 @@ not multiple and the destination field is multiple + + + &Transform tags/authors + + + + + + Create rules to transform metadata by clicking the buttons below. The rules will be applied to all selected books when you click OK. + + + true + + + + + + + + + Transform &tags + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Transform &authors + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Transform &publishers + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + +