EPUB/MOBI Catalogs: Allow saving used settings as presets which can be loaded easily later. Fixes #1155587 ([EHANCEMENT] Save named catalog parameters)

This commit is contained in:
Kovid Goyal 2013-03-19 09:43:21 +05:30
commit 9d90ba326d
4 changed files with 425 additions and 32 deletions

View File

@ -12,20 +12,22 @@ from functools import partial
from calibre.ebooks.conversion.config import load_defaults
from calibre.gui2 import gprefs, open_url, question_dialog
from calibre.utils.config import JSONConfig
from calibre.utils.icu import sort_key
from catalog_epub_mobi_ui import Ui_Form
from PyQt4 import QtGui
from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox,
QDoubleSpinBox, QIcon, QLineEdit, QObject, QRadioButton, QSize, QSizePolicy,
QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, QUrl,
QVBoxLayout, QWidget,
QDoubleSpinBox, QIcon, QInputDialog, QLineEdit, QObject, QRadioButton,
QSize, QSizePolicy, QTableWidget, QTableWidgetItem, QTextEdit, QToolButton,
QUrl, QVBoxLayout, QWidget,
SIGNAL)
class PluginWidget(QWidget,Ui_Form):
TITLE = _('E-book options')
HELP = _('Options specific to')+' AZW3/EPUB/MOBI '+_('output')
DEBUG = False
DEBUG = True
# Output synced to the connected device?
sync_enabled = True
@ -212,8 +214,8 @@ class PluginWidget(QWidget,Ui_Form):
else:
results = _truncated_results(excluded_tags)
finally:
if self.DEBUG:
print(results)
if False and self.DEBUG:
print("exclude_genre_changed(): %s" % results)
self.exclude_genre_results.clear()
self.exclude_genre_results.setText(results)
@ -239,11 +241,11 @@ class PluginWidget(QWidget,Ui_Form):
Toggle Description-related controls
'''
self.header_note_source_field.setEnabled(enabled)
self.thumb_width.setEnabled(enabled)
self.merge_source_field.setEnabled(enabled)
self.merge_before.setEnabled(enabled)
self.merge_after.setEnabled(enabled)
self.include_hr.setEnabled(enabled)
self.merge_after.setEnabled(enabled)
self.merge_before.setEnabled(enabled)
self.merge_source_field.setEnabled(enabled)
self.thumb_width.setEnabled(enabled)
def generate_genres_changed(self, enabled):
'''
@ -263,6 +265,22 @@ class PluginWidget(QWidget,Ui_Form):
self.genre_source_field_name = genre_source_spec['field']
self.exclude_genre_changed()
def get_format_and_title(self):
current_format = None
current_title = None
self.parentWidget().blockSignals(True)
for peer in self.parentWidget().children():
if peer == self:
continue
elif peer.children():
for child in peer.children():
if child.objectName() == 'format':
current_format = str(child.currentText()).strip()
elif child.objectName() == 'title':
current_title = str(child.text()).strip()
self.parentWidget().blockSignals(False)
return current_format, current_title
def header_note_source_field_changed(self,new_index):
'''
Process changes in the header_note_source_field combo box
@ -374,15 +392,20 @@ class PluginWidget(QWidget,Ui_Form):
# Initialize exclusion rules
self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb,
"exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db)
"exclusion_rules_tw", exclusion_rules, self.eligible_custom_fields, self.db)
# Initialize prefix rules
self.prefix_rules_table = PrefixRules(self.prefix_rules_gb,
"prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db)
"prefix_rules_tw", prefix_rules, self.eligible_custom_fields, self.db)
# Initialize excluded genres preview
self.exclude_genre_changed()
# Hook Preset signals
self.preset_delete_pb.clicked.connect(self.preset_remove)
self.preset_save_pb.clicked.connect(self.preset_save)
self.preset_field.currentIndexChanged[str].connect(self.preset_change)
def merge_source_field_changed(self,new_index):
'''
Process changes in the merge_source_field combo box
@ -404,10 +427,12 @@ class PluginWidget(QWidget,Ui_Form):
self.include_hr.setEnabled(False)
def options(self):
# Save/return the current options
# exclude_genre stores literally
# Section switches store as True/False
# others store as lists
'''
Return, optionally save current options
exclude_genre stores literally
Section switches store as True/False
others store as lists
'''
opts_dict = {}
prefix_rules_processed = False
@ -469,7 +494,7 @@ class PluginWidget(QWidget,Ui_Form):
except:
opts_dict['output_profile'] = ['default']
if self.DEBUG:
if False and self.DEBUG:
print "opts_dict"
for opt in sorted(opts_dict.keys(), key=sort_key):
print " %s: %s" % (opt, repr(opts_dict[opt]))
@ -544,6 +569,213 @@ class PluginWidget(QWidget,Ui_Form):
self.genre_source_fields = custom_fields
self.genre_source_field.currentIndexChanged.connect(self.genre_source_field_changed)
# Populate the Presets combo box
self.presets = JSONConfig("catalog_presets")
self.preset_field.addItem("")
self.preset_field_values = sorted([p for p in self.presets], key=sort_key)
self.preset_field.addItems(self.preset_field_values)
def preset_change(self, item_name):
'''
Update catalog options from current preset
'''
if not item_name:
return
current_preset = str(self.preset_field.currentText())
options = self.presets[current_preset]
exclusion_rules = []
prefix_rules = []
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
if c_name == 'preset_field':
continue
# Ignore extra entries in options for cli invocation
if c_name in options:
opt_value = options[c_name]
else:
continue
if c_type in ['check_box']:
getattr(self, c_name).setChecked(eval(str(opt_value)))
if c_name == 'generate_genres':
self.genre_source_field.setEnabled(eval(str(opt_value)))
elif c_type in ['combo_box']:
if opt_value is None:
index = 0
if c_name == 'genre_source_field':
index = self.genre_source_field.findText(_('Tags'))
else:
index = getattr(self,c_name).findText(opt_value)
if index == -1:
if c_name == 'read_source_field':
index = self.read_source_field.findText(_('Tags'))
elif c_name == 'genre_source_field':
index = self.genre_source_field.findText(_('Tags'))
getattr(self,c_name).setCurrentIndex(index)
elif c_type in ['line_edit']:
getattr(self, c_name).setText(opt_value if opt_value else '')
elif c_type in ['radio_button'] and opt_value is not None:
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
getattr(self, c_name).setValue(float(opt_value))
if c_type == 'table_widget':
if c_name == 'exclusion_rules_tw':
if opt_value not in exclusion_rules:
exclusion_rules.append(opt_value)
if c_name == 'prefix_rules_tw':
if opt_value not in prefix_rules:
prefix_rules.append(opt_value)
# Reset exclusion rules
self.exclusion_rules_table.clearLayout()
self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb,
"exclusion_rules_tw", exclusion_rules, self.eligible_custom_fields, self.db)
# Reset prefix rules
self.prefix_rules_table.clearLayout()
self.prefix_rules_table = PrefixRules(self.prefix_rules_gb,
"prefix_rules_tw", prefix_rules, self.eligible_custom_fields, self.db)
# Reset excluded genres preview
self.exclude_genre_changed()
# Reset format and title
format = options['format']
title = options['catalog_title']
self.set_format_and_title(format, title)
def preset_remove(self):
if self.preset_field.currentIndex() == 0:
return
if not question_dialog(self, _("Delete saved catalog preset"),
_("The selected saved catalog preset will be deleted. "
"Are you sure?")):
return
item_id = self.preset_field.currentIndex()
item_name = unicode(self.preset_field.currentText())
self.preset_field.blockSignals(True)
self.preset_field.removeItem(item_id)
self.preset_field.blockSignals(False)
self.preset_field.setCurrentIndex(0)
if item_name in self.presets.keys():
del(self.presets[item_name])
self.presets.commit()
def preset_save(self):
names = ['']
names.extend(self.preset_field_values)
try:
dex = names.index(self.preset_search_name)
except:
dex = 0
name = ''
while not name:
name, ok = QInputDialog.getItem(self, _('Save catalog preset'),
_('Preset name:'), names, dex, True)
if not ok:
return
if not name:
error_dialog(self, _("Save catalog preset"),
_("You must provide a name."), show=True)
new = True
name = unicode(name)
if name in self.presets.keys():
if not question_dialog(self, _("Save catalog preset"),
_("That saved preset already exists and will be overwritten. "
"Are you sure?")):
return
new = False
preset = {}
prefix_rules_processed = False
exclusion_rules_processed = False
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
if c_name == 'exclusion_rules_tw' and exclusion_rules_processed:
continue
if c_name == 'prefix_rules_tw' and prefix_rules_processed:
continue
if c_type in ['check_box', 'radio_button']:
opt_value = getattr(self, c_name).isChecked()
elif c_type in ['combo_box']:
if c_name == 'preset_field':
continue
opt_value = unicode(getattr(self,c_name).currentText()).strip()
elif c_type in ['line_edit']:
opt_value = unicode(getattr(self, c_name).text()).strip()
elif c_type in ['spin_box']:
opt_value = unicode(getattr(self, c_name).value())
elif c_type in ['table_widget']:
if c_name == 'prefix_rules_tw':
opt_value = self.prefix_rules_table.get_data()
prefix_rules_processed = True
if c_name == 'exclusion_rules_tw':
opt_value = self.exclusion_rules_table.get_data()
exclusion_rules_processed = True
preset[c_name] = opt_value
# Construct cli version of table rules
if c_name in ['exclusion_rules_tw','prefix_rules_tw']:
self.construct_tw_opts_object(c_name, opt_value, preset)
format, title = self.get_format_and_title()
preset['format'] = format
preset['catalog_title'] = title
# Additional items needed for cli invocation
# Generate specs for merge_comments, header_note_source_field, genre_source_field
checked = ''
if self.merge_before.isChecked():
checked = 'before'
elif self.merge_after.isChecked():
checked = 'after'
include_hr = self.include_hr.isChecked()
preset['merge_comments_rule'] = "%s:%s:%s" % \
(self.merge_source_field_name, checked, include_hr)
preset['header_note_source_field'] = self.header_note_source_field_name
preset['genre_source_field'] = self.genre_source_field_name
# Append the current output profile
try:
preset['output_profile'] = load_defaults('page_setup')['output_profile']
except:
preset['output_profile'] = 'default'
self.presets[name] = preset
self.presets.commit()
if new:
self.preset_field.blockSignals(True)
self.preset_field.clear()
self.preset_field.addItem('')
self.preset_field_values = sorted([q for q in self.presets], key=sort_key)
self.preset_field.addItems(self.preset_field_values)
self.preset_field.blockSignals(False)
self.preset_field.setCurrentIndex(self.preset_field.findText(name))
def set_format_and_title(self, format, title):
for peer in self.parentWidget().children():
if peer == self:
continue
elif peer.children():
for child in peer.children():
if child.objectName() == 'format':
index = child.findText(format)
child.blockSignals(True)
child.setCurrentIndex(index)
child.blockSignals(False)
elif child.objectName() == 'title':
child.setText(title)
def show_help(self):
'''
Display help file
@ -631,6 +863,7 @@ class GenericRulesTable(QTableWidget):
self.last_row_selected = self.currentRow()
self.last_rows_selected = self.selectionModel().selectedRows()
# Add the controls
self._init_controls()
# Hook check_box changes
@ -681,6 +914,21 @@ class GenericRulesTable(QTableWidget):
# In case table was empty
self.horizontalHeader().setStretchLastSection(True)
def clearLayout(self):
if self.layout is not None:
old_layout = self.layout
for child in old_layout.children():
for i in reversed(range(child.count())):
if child.itemAt(i).widget() is not None:
child.itemAt(i).widget().setParent(None)
import sip
sip.delete(child)
for i in reversed(range(old_layout.count())):
if old_layout.itemAt(i).widget() is not None:
old_layout.itemAt(i).widget().setParent(None)
def delete_row(self):
if self.DEBUG:
print("%s:delete_row()" % self.objectName())

View File

@ -20,6 +20,54 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="catalogPresets">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Presets</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QComboBox" name="preset_field">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Select catalog preset to load</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="preset_save_pb">
<property name="toolTip">
<string>Save current catalog settings as preset</string>
</property>
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="preset_delete_pb">
<property name="toolTip">
<string>Delete current preset</string>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="includedSections">
<property name="sizePolicy">
@ -46,6 +94,9 @@
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>List of books, sorted by Author</string>
</property>
<property name="text">
<string>&amp;Authors</string>
</property>
@ -54,15 +105,21 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QCheckBox" name="generate_titles">
<property name="toolTip">
<string>List of books, sorted by Title</string>
</property>
<property name="text">
<string>&amp;Titles</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QCheckBox" name="generate_series">
<property name="toolTip">
<string>List of series books, sorted by Series</string>
</property>
<property name="text">
<string>&amp;Series</string>
</property>
@ -72,6 +129,9 @@
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="generate_genres">
<property name="toolTip">
<string>List of books, sorted by Genre</string>
</property>
<property name="text">
<string>&amp;Genres</string>
</property>
@ -80,13 +140,13 @@
<item>
<widget class="QComboBox" name="genre_source_field">
<property name="toolTip">
<string>Field containing Genre information</string>
<string>Field containing Genres</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="2">
<item row="2" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="generate_recently_added">
@ -96,6 +156,9 @@
<height>26</height>
</size>
</property>
<property name="toolTip">
<string>List of books, sorted by date added to calibre</string>
</property>
<property name="text">
<string>&amp;Recently Added</string>
</property>
@ -103,7 +166,7 @@
</item>
</layout>
</item>
<item row="3" column="2">
<item row="4" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QCheckBox" name="generate_descriptions">
@ -113,6 +176,9 @@
<height>26</height>
</size>
</property>
<property name="toolTip">
<string>Individual descriptions of books with cover thumbs, sorted by author</string>
</property>
<property name="text">
<string>&amp;Descriptions</string>
</property>
@ -120,6 +186,41 @@
</item>
</layout>
</item>
<item row="0" column="1" rowspan="5">
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="Line" name="line_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="Line" name="line_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -347,7 +448,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
</size>
</property>
<property name="toolTip">
<string>Custom column containing additional content to be merged with Comments metadata.</string>
<string>Custom column containing additional content to be merged with Comments metadata in Descriptions section.</string>
</property>
</widget>
</item>
@ -361,7 +462,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<item>
<widget class="QRadioButton" name="merge_before">
<property name="toolTip">
<string>Merge additional content before Comments metadata.</string>
<string>Merge additional content before Comments in Descriptions section.</string>
</property>
<property name="text">
<string>&amp;Before</string>
@ -374,7 +475,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<item>
<widget class="QRadioButton" name="merge_after">
<property name="toolTip">
<string>Merge additional content after Comments metadata.</string>
<string>Merge additional content after Comments in Descriptions section.</string>
</property>
<property name="text">
<string>&amp;After</string>
@ -394,7 +495,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<item>
<widget class="QCheckBox" name="include_hr">
<property name="toolTip">
<string>Separate Comments metadata and additional content with a horizontal rule.</string>
<string>Separate Comments metadata and additional content with a horizontal rule in Descriptions section.</string>
</property>
<property name="text">
<string>Include &amp;Separator</string>
@ -514,7 +615,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
</size>
</property>
<property name="toolTip">
<string>Custom column source for text to include in Description section.</string>
<string>Custom column source for text to include in Descriptions section.</string>
</property>
</widget>
</item>

View File

@ -11,17 +11,18 @@ import os
from collections import namedtuple
from calibre import strftime
from calibre.constants import config_dir
from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks import calibre_cover
from calibre.library import current_library_name
from calibre.library.catalogs import AuthorSortMismatchException, EmptyCatalogException
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import JSONConfig
from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang, get_lang
Option = namedtuple('Option', 'option, default, dest, action, help')
class EPUB_MOBI(CatalogPlugin):
'ePub catalog generator'
@ -162,6 +163,14 @@ class EPUB_MOBI(CatalogPlugin):
"When multiple rules are defined, the first matching rule will be used.\n"
"Default:\n" + '"' + '%default' + '"' + "\n"
"Applies to AZW3, ePub, MOBI output formats")),
Option('--preset',
default=None,
dest='preset',
action=None,
help=_("Use a named preset created with the GUI Catalog builder.\n"
"A preset specifies all settings for building a catalog.\n"
"Default: '%default'\n"
"Applies to AZW3, ePub, MOBI output formats")),
Option('--use-existing-cover',
default=False,
dest='use_existing_cover',
@ -184,6 +193,43 @@ class EPUB_MOBI(CatalogPlugin):
from calibre.library.catalogs.epub_mobi_builder import CatalogBuilder
from calibre.utils.logging import default_log as log
# If preset specified from the cli, insert stored options from JSON file
if hasattr(opts, 'preset') and opts.preset:
available_presets = JSONConfig("catalog_presets")
if not opts.preset in available_presets:
if available_presets:
print(_('Error: Preset "%s" not found.' % opts.preset))
print(_('Stored presets: %s' % ', '.join([p for p in sorted(available_presets.keys())])))
else:
print(_('Error: No stored presets.'))
return 1
# Copy the relevant preset values to the opts object
for item in available_presets[opts.preset]:
if not item in ['exclusion_rules_tw', 'format', 'prefix_rules_tw']:
setattr(opts, item, available_presets[opts.preset][item])
# Provide an unconnected device
opts.connected_device = {
'is_device_connected': False,
'kind': None,
'name': None,
'save_template': None,
'serial': None,
'storage': None,
}
# Convert prefix_rules and exclusion_rules from JSON lists to tuples
prs = []
for rule in opts.prefix_rules:
prs.append(tuple(rule))
opts.prefix_rules = tuple(prs)
ers = []
for rule in opts.exclusion_rules:
ers.append(tuple(rule))
opts.exclusion_rules = tuple(ers)
opts.log = log
opts.fmt = self.fmt = path_to_output.rpartition('.')[2]
@ -329,15 +375,14 @@ class EPUB_MOBI(CatalogPlugin):
log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule))
# Display opts
keys = opts_dict.keys()
keys.sort()
keys = sorted(opts_dict.keys())
build_log.append(" opts:")
for key in keys:
if key in ['catalog_title', 'author_clip', 'connected_kindle', 'creator',
'cross_reference_authors', 'description_clip', 'exclude_book_marker',
'exclude_genre', 'exclude_tags', 'exclusion_rules', 'fmt',
'genre_source_field', 'header_note_source_field', 'merge_comments_rule',
'output_profile', 'prefix_rules', 'read_book_marker',
'output_profile', 'prefix_rules', 'preset', 'read_book_marker',
'search_text', 'sort_by', 'sort_descriptions_by_author', 'sync',
'thumb_width', 'use_existing_cover', 'wishlist_tag']:
build_log.append(" %s: %s" % (key, repr(opts_dict[key])))

View File

@ -1277,7 +1277,6 @@ class CatalogBuilder(object):
self.opts.log.info('%s' % _format_tag_list(genre_tags_dict, header="enabled genres"))
self.opts.log.info('%s' % _format_tag_list(excluded_tags, header="excluded genres"))
print("genre_tags_dict: %s" % genre_tags_dict)
return genre_tags_dict
def filter_excluded_genres(self, tags, regex):