Bulk metadata edit: Allow choosing the algorithm used for changing the case fo titles in bulk

This commit is contained in:
Kovid Goyal 2017-12-12 09:24:44 +05:30
parent 21205e69ef
commit 8301cdd8ed
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 120 additions and 72 deletions

View File

@ -1,47 +1,59 @@
__license__ = 'GPL v3' #!/usr/bin/env python2
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' # vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2008, Kovid Goyal <kovid at kovidgoyal.net>
'''Dialog to edit metadata in bulk'''
import re import re
from collections import defaultdict, namedtuple
from io import BytesIO from io import BytesIO
from collections import namedtuple, defaultdict
from threading import Thread from threading import Thread
from PyQt5.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \ from PyQt5.Qt import (
pyqtSignal, QDialogButtonBox, QInputDialog, QLineEdit, \ QCompleter, QCoreApplication, QDateTime, QDialog, QDialogButtonBox, QFont,
QDateTime, QCompleter, QCoreApplication, QSize QGridLayout, QInputDialog, QLabel, QLineEdit, QSize, Qt, QVBoxLayout, pyqtSignal
)
from calibre import prints from calibre import prints
from calibre.ebooks.metadata.opf2 import OPF
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.db import _get_next_series_num_for_list
from calibre.ebooks.metadata import authors_to_string, string_to_authors, title_sort
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.ebooks.metadata.opf2 import OPF
from calibre.gui2 import (
UNDEFINED_QDATETIME, FunctionDispatcher, error_dialog, gprefs, question_dialog
)
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.ebooks.metadata import string_to_authors, authors_to_string, title_sort
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog, UNDEFINED_QDATETIME, \
gprefs, question_dialog, FunctionDispatcher
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.metadata.basic_widgets import CalendarWidget from calibre.gui2.metadata.basic_widgets import CalendarWidget
from calibre.utils.config import dynamic, JSONConfig from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.titlecase import titlecase from calibre.utils.config import JSONConfig, dynamic, prefs, tweaks
from calibre.utils.icu import sort_key, capitalize
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt from calibre.utils.date import qt_to_dt
from calibre.db import _get_next_series_num_for_list from calibre.utils.icu import capitalize, sort_key
from calibre.utils.titlecase import titlecase
from calibre.gui2.widgets import LineEditECM
Settings = namedtuple('Settings', Settings = namedtuple('Settings',
'remove_all remove add au aus do_aus rating pub do_series do_autonumber ' '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_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 ' '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') 'restore_original comments generate_cover_settings read_file_metadata casing_algorithm')
null = object() null = object()
class Caser(LineEditECM):
def __init__(self, title):
self.title = title
def text(self):
return self.title
def setText(self, text):
self.title = text
class MyBlockingBusy(QDialog): # {{{ class MyBlockingBusy(QDialog): # {{{
all_done = pyqtSignal() all_done = pyqtSignal()
@ -154,6 +166,11 @@ class MyBlockingBusy(QDialog): # {{{
if old != prefs['read_file_metadata']: if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old prefs['read_file_metadata'] = old
def change_title_casing(val):
caser = Caser(val)
getattr(caser, args.casing_algorithm)()
return caser.title
# Title and authors # Title and authors
if args.do_swap_ta: if args.do_swap_ta:
title_map = cache.all_field_for('title', self.ids) title_map = cache.all_field_for('title', self.ids)
@ -161,7 +178,7 @@ class MyBlockingBusy(QDialog): # {{{
def new_title(authors): def new_title(authors):
ans = authors_to_string(authors) ans = authors_to_string(authors)
return titlecase(ans) if args.do_title_case else ans return change_title_casing(ans) if args.do_title_case else ans
new_title_map = {bid:new_title(authors) for bid, authors in authors_map.iteritems()} new_title_map = {bid:new_title(authors) for bid, authors in authors_map.iteritems()}
new_authors_map = {bid:string_to_authors(title) for bid, title in title_map.iteritems()} new_authors_map = {bid:string_to_authors(title) for bid, title in title_map.iteritems()}
cache.set_field('authors', new_authors_map) cache.set_field('authors', new_authors_map)
@ -169,7 +186,7 @@ class MyBlockingBusy(QDialog): # {{{
if args.do_title_case and not args.do_swap_ta: if args.do_title_case and not args.do_swap_ta:
title_map = cache.all_field_for('title', self.ids) title_map = cache.all_field_for('title', self.ids)
cache.set_field('title', {bid:titlecase(title) for bid, title in title_map.iteritems()}) cache.set_field('title', {bid:change_title_casing(title) for bid, title in title_map.iteritems()})
if args.do_title_sort: if args.do_title_sort:
lang_map = cache.all_field_for('languages', self.ids) lang_map = cache.all_field_for('languages', self.ids)
@ -363,6 +380,15 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.adddate.setSpecialValueText(_('Undefined')) self.adddate.setSpecialValueText(_('Undefined'))
self.clear_adddate_button.clicked.connect(self.clear_adddate) self.clear_adddate_button.clicked.connect(self.clear_adddate)
self.adddate.dateTimeChanged.connect(self.do_apply_adddate) self.adddate.dateTimeChanged.connect(self.do_apply_adddate)
self.casing_algorithm.addItems([
_('Title case'), _('Capitalize'), _('Upper case'), _('Lower case'), _('Swap case')
])
self.casing_map = ['title_case', 'capitalize', 'upper_case', 'lower_case', 'swap_case']
prevca = gprefs.get('bulk-mde-casing-algorithm', 'title_case')
idx = max(0, self.casing_map.index(prevca))
self.casing_algorithm.setCurrentIndex(idx)
self.casing_algorithm.setEnabled(False)
self.change_title_to_title_case.toggled.connect(lambda : self.casing_algorithm.setEnabled(self.change_title_to_title_case.isChecked()))
if len(self.db.custom_field_keys(include_composites=False)) == 0: if len(self.db.custom_field_keys(include_composites=False)) == 0:
self.central_widget.removeTab(1) self.central_widget.removeTab(1)
@ -1047,7 +1073,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
do_title_case, cover_action, clear_series, clear_pub, pubdate, do_title_case, cover_action, clear_series, clear_pub, pubdate,
adddate, do_title_sort, languages, clear_languages, adddate, do_title_sort, languages, clear_languages,
restore_original, self.comments, self.generate_cover_settings, restore_original, self.comments, self.generate_cover_settings,
read_file_metadata) read_file_metadata, self.casing_map[self.casing_algorithm.currentIndex()])
if DEBUG: if DEBUG:
print('Running bulk metadata operation with settings:') print('Running bulk metadata operation with settings:')
print(args) print(args)
@ -1073,6 +1099,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
show=True) show=True)
dynamic['s_r_search_mode'] = self.search_mode.currentIndex() dynamic['s_r_search_mode'] = self.search_mode.currentIndex()
gprefs.set('bulk-mde-casing-algorithm', args.casing_algorithm)
self.db.clean() self.db.clean()
return QDialog.accept(self) return QDialog.accept(self)

View File

@ -70,8 +70,8 @@ is completed. This can be slow on large libraries.</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>944</width> <width>946</width>
<height>638</height> <height>661</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
@ -97,6 +97,22 @@ is completed. This can be slow on large libraries.</string>
<string>&amp;Basic metadata</string> <string>&amp;Basic metadata</string>
</attribute> </attribute>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Ser&amp;ies:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="9" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_10"> <widget class="QLabel" name="label_10">
<property name="text"> <property name="text">
@ -130,20 +146,6 @@ is completed. This can be slow on large libraries.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="3">
<widget class="QToolButton" name="tag_editor_button">
<property name="toolTip">
<string>Open Tag editor</string>
</property>
<property name="text">
<string>Open Tag editor</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
<item row="1" column="2"> <item row="1" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
@ -168,6 +170,20 @@ is completed. This can be slow on large libraries.</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="5" column="3">
<widget class="QToolButton" name="tag_editor_button">
<property name="toolTip">
<string>Open Tag editor</string>
</property>
<property name="text">
<string>Open Tag editor</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
@ -181,7 +197,7 @@ is completed. This can be slow on large libraries.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="0"> <item row="19" column="0">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -308,22 +324,6 @@ for e.g., EPUB to EPUB, calibre saves the original EPUB
<item row="3" column="2"> <item row="3" column="2">
<widget class="RatingEditor" name="rating"/> <widget class="RatingEditor" name="rating"/>
</item> </item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Ser&amp;ies:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="11" column="0"> <item row="11" column="0">
<widget class="QLabel" name="label_11"> <widget class="QLabel" name="label_11">
<property name="text"> <property name="text">
@ -514,7 +514,7 @@ from the value in the box</string>
<double>0.000000000000000</double> <double>0.000000000000000</double>
</property> </property>
<property name="maximum"> <property name="maximum">
<double>99999.0</double> <double>99999.000000000000000</double>
</property> </property>
<property name="value"> <property name="value">
<double>1.000000000000000</double> <double>1.000000000000000</double>
@ -589,23 +589,36 @@ from the value in the box</string>
title and author are swapped before the title case is set</string> title and author are swapped before the title case is set</string>
</property> </property>
<property name="text"> <property name="text">
<string>Change title to title &amp;case</string> <string>Change title &amp;case to:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="update_title_sort"> <widget class="QComboBox" name="casing_algorithm">
<property name="toolTip"> <property name="minimumSize">
<string>Update title sort based on the current title. This will be applied only after other changes to title.</string> <size>
</property> <width>150</width>
<property name="text"> <height>0</height>
<string>Update &amp;title sort</string> </size>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="horizontalSpacer">
<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> </layout>
</item> </item>
<item row="16" column="0" colspan="4"> <item row="17" column="0" colspan="4">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Change &amp;cover</string> <string>Change &amp;cover</string>
@ -658,7 +671,7 @@ as that of the first selected book.</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="17" column="0" colspan="4"> <item row="18" column="0" colspan="4">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QPushButton" name="button_config_cover_gen"> <widget class="QPushButton" name="button_config_cover_gen">
@ -708,6 +721,16 @@ as that of the first selected book.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="16" column="0">
<widget class="QCheckBox" name="update_title_sort">
<property name="toolTip">
<string>Update title sort based on the current title. This will be applied only after other changes to title.</string>
</property>
<property name="text">
<string>Update &amp;title sort</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">
@ -1194,8 +1217,8 @@ not multiple and the destination field is multiple</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>203</width> <width>924</width>
<height>70</height> <height>311</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="testgrid"> <layout class="QGridLayout" name="testgrid">
@ -1308,8 +1331,6 @@ not multiple and the destination field is multiple</string>
<tabstop>languages</tabstop> <tabstop>languages</tabstop>
<tabstop>clear_languages</tabstop> <tabstop>clear_languages</tabstop>
<tabstop>restore_original</tabstop> <tabstop>restore_original</tabstop>
<tabstop>change_title_to_title_case</tabstop>
<tabstop>update_title_sort</tabstop>
<tabstop>cover_generate</tabstop> <tabstop>cover_generate</tabstop>
<tabstop>cover_remove</tabstop> <tabstop>cover_remove</tabstop>
<tabstop>cover_from_fmt</tabstop> <tabstop>cover_from_fmt</tabstop>