WIP - changes to support prefix rules

This commit is contained in:
GRiker 2012-08-03 03:23:15 -06:00
parent c28175adfc
commit add5bb824d
6 changed files with 437 additions and 369 deletions

View File

@ -151,14 +151,23 @@ p.title {
font-size:xx-large;
}
p.wishlist_item, p.unread_book, p.read_book {
text-align:left;
p.wishlist_item, p.unread_book, p.read_book, p.line_item {
font-family:monospace;
margin-top:0px;
margin-bottom:0px;
margin-left:2em;
text-align:left;
text-indent:-2em;
}
span.prefix {}
span.entry {
font-family: serif;
}
/*
* Book Descriptions
*/
td.publisher, td.date {
font-weight:bold;
text-align:center;

View File

@ -10,8 +10,11 @@ from calibre.ebooks.conversion.config import load_defaults
from calibre.gui2 import gprefs
from catalog_epub_mobi_ui import Ui_Form
from PyQt4.Qt import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, \
QRadioButton, QWidget
from PyQt4 import QtGui
from PyQt4.Qt import (Qt, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox,
QHBoxLayout, QIcon, QLabel, QLineEdit,
QPlainTextEdit, QRadioButton, QSize, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget)
class PluginWidget(QWidget,Ui_Form):
@ -36,6 +39,7 @@ class PluginWidget(QWidget,Ui_Form):
DoubleSpinBoxControls = []
LineEditControls = []
RadioButtonControls = []
TableWidgetControls = []
for item in self.__dict__:
if type(self.__dict__[item]) is QCheckBox:
@ -48,6 +52,8 @@ class PluginWidget(QWidget,Ui_Form):
LineEditControls.append(str(self.__dict__[item].objectName()))
elif type(self.__dict__[item]) is QRadioButton:
RadioButtonControls.append(str(self.__dict__[item].objectName()))
elif type(self.__dict__[item]) is QTableWidget:
TableWidgetControls.append(str(self.__dict__[item].objectName()))
option_fields = zip(CheckBoxControls,
[True for i in CheckBoxControls],
@ -60,15 +66,41 @@ class PluginWidget(QWidget,Ui_Form):
['radio_button' for i in RadioButtonControls])
# LineEditControls
option_fields += zip(['exclude_genre'],['\[.+\]'],['line_edit'])
option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit'])
option_fields += zip(['exclude_pattern'],[None],['line_edit'])
option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
option_fields += zip(['read_pattern'],['+'],['line_edit'])
option_fields += zip(['wishlist_tag'],['Wishlist'],['line_edit'])
# SpinBoxControls
option_fields += zip(['thumb_width'],[1.00],['spin_box'])
# Prefix rules TableWidget
# ordinal/enabled/rule name/source field/pattern/prefix
#option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'],
# [(1,True,'Read book','Tags','+','+'),
# (2,True,'Wishlist','Tags','Wishlist','x'),
# (3,False,'<new rule>','','','')],
# ['table_widget','table_widget','table_widget'])
option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'],
[{'ordinal':1,
'enabled':True,
'name':'Read book',
'field':'Tags',
'pattern':'+',
'prefix':'+'},
{'ordinal':2,
'enabled':True,
'name':'Wishlist',
'field':'Tags',
'pattern':'Wishlist',
'prefix':'x'},
{'ordinal':3,
'enabled':False,
'name':'<new rule>',
'field':'',
'pattern':'',
'prefix':''}],
['table_widget','table_widget','table_widget'])
self.OPTION_FIELDS = option_fields
def initialize(self, name, db):
@ -78,23 +110,25 @@ class PluginWidget(QWidget,Ui_Form):
['generate_titles','generate_series','generate_genres',
'generate_recently_added','generate_descriptions','include_hr']
ComboBoxControls (c_type: combo_box):
['read_source_field','exclude_source_field','header_note_source_field',
['exclude_source_field','header_note_source_field',
'merge_source_field']
LineEditControls (c_type: line_edit):
['exclude_genre','exclude_pattern','exclude_tags','read_pattern',
'wishlist_tag']
['exclude_genre','exclude_pattern','exclude_tags']
RadioButtonControls (c_type: radio_button):
['merge_before','merge_after']
SpinBoxControls (c_type: spin_box):
['thumb_width']
TableWidgetControls (c_type: table_widget):
['prefix_rules_tw']
'''
self.name = name
self.db = db
self.populateComboBoxes()
self.all_custom_fields = self.db.custom_field_keys()
self.populate_combo_boxes()
# Update dialog fields from stored options
prefix_rules = []
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
opt_value = gprefs.get(self.name + '_' + c_name, c_def)
@ -114,11 +148,8 @@ class PluginWidget(QWidget,Ui_Form):
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
getattr(self, c_name).setValue(float(opt_value))
# Init self.read_source_field_name
cs = unicode(self.read_source_field.currentText())
read_source_spec = self.read_source_fields[cs]
self.read_source_field_name = read_source_spec['field']
elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw':
prefix_rules.append(opt_value)
# Init self.exclude_source_field_name
self.exclude_source_field_name = ''
@ -147,6 +178,32 @@ class PluginWidget(QWidget,Ui_Form):
# Hook changes to Description section
self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
# Init prefix_rules
self.initialize_prefix_rules(prefix_rules)
self.populate_prefix_rules(prefix_rules)
def initialize_prefix_rules(self, rules):
# Assign icons to buttons
self.move_rule_up_tb.setToolTip('Move rule up')
self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png')))
self.add_rule_tb.setToolTip('Add a new rule')
self.add_rule_tb.setIcon(QIcon(I('plus.png')))
self.delete_rule_tb.setToolTip('Delete selected rule')
self.delete_rule_tb.setIcon(QIcon(I('list_remove.png')))
self.move_rule_down_tb.setToolTip('Move rule down')
self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png')))
# Configure the QTableWidget
# enabled/rule name/source field/pattern/prefix
self.prefix_rules_tw.clear()
header_labels = ['','Name','Source','Pattern','Prefix']
self.prefix_rules_tw.setColumnCount(len(header_labels))
self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels)
self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True)
self.prefix_rules_tw.setRowCount(len(rules))
self.prefix_rules_tw.setSortingEnabled(False)
def options(self):
# Save/return the current options
# exclude_genre stores literally
@ -203,7 +260,7 @@ class PluginWidget(QWidget,Ui_Form):
print " %s: %s" % (opt, repr(opts_dict[opt]))
return opts_dict
def populateComboBoxes(self):
def populate_combo_boxes(self):
# Custom column types declared in
# gui2.preferences.create_custom_column:CreateCustomColumn()
# As of 0.7.34:
@ -219,25 +276,9 @@ class PluginWidget(QWidget,Ui_Form):
# text Column shown in the tag browser
# *text Comma-separated text, like tags, shown in tag browser
all_custom_fields = self.db.custom_field_keys()
# Populate the 'Read book' hybrid
custom_fields = {}
custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.read_source_field.addItem(cf)
self.read_source_fields = custom_fields
self.read_source_field.currentIndexChanged.connect(self.read_source_field_changed)
# Populate the 'Excluded books' hybrid
custom_fields = {}
for custom_field in all_custom_fields:
for custom_field in self.all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
@ -253,7 +294,7 @@ class PluginWidget(QWidget,Ui_Form):
# Populate the 'Header note' combo box
custom_fields = {}
for custom_field in all_custom_fields:
for custom_field in self.all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
@ -269,7 +310,7 @@ class PluginWidget(QWidget,Ui_Form):
# Populate the 'Merge with Comments' combo box
custom_fields = {}
for custom_field in all_custom_fields:
for custom_field in self.all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['text','comments','composite']:
custom_fields[field_md['name']] = {'field':custom_field,
@ -285,6 +326,42 @@ class PluginWidget(QWidget,Ui_Form):
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
def populate_prefix_rules(self,rules):
def populate_table_row(row, data):
self.prefix_rules_tw.blockSignals(True)
self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled']))
self.prefix_rules_tw.setItem(row, 1, QTableWidgetItem(data['name']))
set_source_field_in_row(row, field=data['field'])
self.prefix_rules_tw.setItem(row, 3, QTableWidgetItem(data['pattern']))
self.prefix_rules_tw.setItem(row, 4, QTableWidgetItem(data['prefix']))
self.prefix_rules_tw.blockSignals(False)
def set_source_field_in_row(row, field=''):
custom_fields = {}
custom_fields['Tags'] = {'field':'tag', 'datatype':u'text'}
for custom_field in self.all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
self.prefix_rules_tw.setCellWidget(row, 2, SourceFieldComboBox(self, sorted(custom_fields.keys()), field))
# Need to connect to something that monitors changes to control pattern field based on source_field type
# Entry point
for row, data in enumerate(sorted(rules, key=lambda k: k['ordinal'])):
populate_table_row(row, data)
# Do this after populating cells
self.prefix_rules_tw.resizeColumnsToContents()
def read_source_field_changed(self,new_index):
'''
Process changes in the read_source_field combo box
@ -388,3 +465,55 @@ class PluginWidget(QWidget,Ui_Form):
Process changes in the thumb_width spin box
'''
pass
class CheckableTableWidgetItem(QTableWidgetItem):
'''
Borrowed from kiwidude
'''
def __init__(self, checked=False, is_tristate=False):
QTableWidgetItem.__init__(self, '')
self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ))
if is_tristate:
self.setFlags(self.flags() | Qt.ItemIsTristate)
if checked:
self.setCheckState(Qt.Checked)
else:
if is_tristate and checked is None:
self.setCheckState(Qt.PartiallyChecked)
else:
self.setCheckState(Qt.Unchecked)
def get_boolean_value(self):
'''
Return a boolean value indicating whether checkbox is checked
If this is a tristate checkbox, a partially checked value is returned as None
'''
if self.checkState() == Qt.PartiallyChecked:
return None
else:
return self.checkState() == Qt.Checked
class NoWheelComboBox(QComboBox):
def wheelEvent (self, event):
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
event.ignore()
class SourceFieldComboBox(NoWheelComboBox):
def __init__(self, parent, format_names, selected_text):
NoWheelComboBox.__init__(self, parent)
self.populate_combo(format_names, selected_text)
def populate_combo(self, format_names, selected_text):
self.addItems([''])
self.addItems(format_names)
if selected_text:
idx = self.findText(selected_text)
self.setCurrentIndex(idx)
else:
self.setCurrentIndex(0)

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>650</width>
<height>596</height>
<height>603</height>
</rect>
</property>
<property name="sizePolicy">
@ -203,6 +203,9 @@ e.g., [Project Gutenberg]&lt;/p&gt;</string>
<string>Excluded books</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
@ -323,97 +326,67 @@ Default: ~,Catalog</string>
</widget>
</item>
<item>
<widget class="QGroupBox" name="readBooks">
<widget class="QGroupBox" name="prefixRules">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Matching books will be displayed with a check mark</string>
</property>
<property name="title">
<string>Read books</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="read_spec_hl">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
<string>Prefix rules</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_3">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QTableWidget" name="prefix_rules_tw">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
<width>16777215</width>
<height>118</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QToolButton" name="move_rule_up_tb">
<property name="text">
<string>&amp;Column/value</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>read_source_field</cstring>
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="read_source_field">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Column containing 'read' status</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>18</number>
<widget class="QToolButton" name="add_rule_tb">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="read_pattern">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>'read book' pattern</string>
</property>
<property name="statusTip">
<string/>
<widget class="QToolButton" name="delete_rule_tb">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="move_rule_down_tb">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
@ -440,49 +413,7 @@ Default: ~,Catalog</string>
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_5">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>&amp;Wishlist tag</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>wishlist_tag</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="wishlist_tag">
<property name="toolTip">
<string>Books tagged as Wishlist items will be displayed with an X</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_4">
@ -542,7 +473,7 @@ Default: ~,Catalog</string>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_6">
@ -599,7 +530,7 @@ Default: ~,Catalog</string>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_9">

View File

@ -51,7 +51,8 @@ class EPUB_MOBI(CatalogPlugin):
default=':',
dest='exclude_book_marker',
action = None,
help=_("field:pattern specifying custom field/contents indicating book should be excluded.\n"
help=_("#<custom field>:pattern specifying custom field/contents indicating book should be excluded.\n"
"For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n"
"Default: '%default'\n"
"Applies to ePub, MOBI output formats")),
Option('--exclude-genre',
@ -121,7 +122,7 @@ class EPUB_MOBI(CatalogPlugin):
default='::',
dest='merge_comments',
action = None,
help=_("<custom field>:[before|after]:[True|False] specifying:\n"
help=_("#<custom field>:[before|after]:[True|False] specifying:\n"
" <custom field> Custom field containing notes to merge with Comments\n"
" [before|after] Placement of notes with respect to Comments\n"
" [True|False] - A horizontal rule is inserted between notes and Comments\n"
@ -134,11 +135,14 @@ class EPUB_MOBI(CatalogPlugin):
help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--read-book-marker',
default='tag:+',
dest='read_book_marker',
action = None,
help=_("field:pattern indicating book has been read.\n" "Default: '%default'\n"
Option('--prefix-rules',
default="(('Read books','tags','+','\u2713'),('Wishlist items','tags','Wishlist','\u00d7'))",
dest='prefix_rules',
action=None,
help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n"
"The model for a prefix rule is ('<rule name>','<source field>','<pattern>','<prefix>').\n"
"When multiple rules are defined, the first matching rule will be used.\n"
"Default: '%default'\n"
"Applies to ePub, MOBI output formats")),
Option('--thumb-width',
default='1.0',
@ -148,12 +152,6 @@ class EPUB_MOBI(CatalogPlugin):
"Range: 1.0 - 2.0\n"
"Default: '%default'\n"
"Applies to ePub, MOBI output formats")),
Option('--wishlist-tag',
default='Wishlist',
dest='wishlist_tag',
action = None,
help=_("Tag indicating book to be displayed as wishlist item.\n" "Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
]
# }}}
@ -276,6 +274,15 @@ class EPUB_MOBI(CatalogPlugin):
log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_SMALLEST))
opts.thumb_width = "1.0"
# Pre-process prefix_rules
try:
opts.prefix_rules = eval(opts.prefix_rules)
except:
log.error("malformed --prefix-rules: %s" % opts.prefix_rules)
raise
for rule in opts.prefix_rules:
if len(rule) != 4:
log.error("incorrect number of args for --prefix-rules: %s" % repr(rule))
# Display opts
keys = opts_dict.keys()
@ -285,7 +292,7 @@ class EPUB_MOBI(CatalogPlugin):
if key in ['catalog_title','authorClip','connected_kindle','descriptionClip',
'exclude_book_marker','exclude_genre','exclude_tags',
'header_note_source_field','merge_comments',
'output_profile','read_book_marker',
'output_profile','prefix_rules','read_book_marker',
'search_text','sort_by','sort_descriptions_by_author','sync',
'thumb_width','wishlist_tag']:
build_log.append(" %s: %s" % (key, repr(opts_dict[key])))

View File

@ -72,6 +72,7 @@ class CatalogBuilder(object):
self.__currentStep = 0.0
self.__creator = opts.creator
self.__db = db
self.__defaultPrefix = None
self.__descriptionClip = opts.descriptionClip
self.__error = []
self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \
@ -91,10 +92,9 @@ class CatalogBuilder(object):
self.__output_profile = None
self.__playOrder = 1
self.__plugin = plugin
self.__prefixRules = []
self.__progressInt = 0.0
self.__progressString = ''
f, _, p = opts.read_book_marker.partition(':')
self.__read_book_marker = {'field':f, 'pattern':p}
f, p, hr = self.opts.merge_comments.split(':')
self.__merge_comments = {'field':f, 'position':p, 'hr':hr}
self.__reporter = report_progress
@ -113,6 +113,9 @@ class CatalogBuilder(object):
self.__output_profile = profile
break
# Process prefix rules
self.processPrefixRules()
# Confirm/create thumbs archive.
if self.opts.generate_descriptions:
if not os.path.exists(self.__cache_dir):
@ -269,6 +272,13 @@ class CatalogBuilder(object):
return self.__db
return property(fget=fget)
@dynamic_property
def defaultPrefix(self):
def fget(self):
return self.__defaultPrefix
def fset(self, val):
self.__defaultPrefix = val
return property(fget=fget, fset=fset)
@dynamic_property
def descriptionClip(self):
def fget(self):
return self.__descriptionClip
@ -363,6 +373,13 @@ class CatalogBuilder(object):
return self.__plugin
return property(fget=fget)
@dynamic_property
def prefixRules(self):
def fget(self):
return self.__prefixRules
def fset(self, val):
self.__prefixRules = val
return property(fget=fget, fset=fset)
@dynamic_property
def progressInt(self):
def fget(self):
return self.__progressInt
@ -437,27 +454,12 @@ class CatalogBuilder(object):
return property(fget=fget, fset=fset)
@dynamic_property
def MISSING_SYMBOL(self):
def fget(self):
return self.__output_profile.missing_char
return property(fget=fget)
@dynamic_property
def NOT_READ_SYMBOL(self):
def fget(self):
return '<span style="color:white">%s</span>' % self.__output_profile.read_char
return property(fget=fget)
@dynamic_property
def READING_SYMBOL(self):
def fget(self):
return '<span style="color:black">&#x25b7;</span>' if self.generateForKindle else \
'<span style="color:white">+</span>'
return property(fget=fget)
@dynamic_property
def READ_SYMBOL(self):
def fget(self):
return self.__output_profile.read_char
return property(fget=fget)
@dynamic_property
def FULL_RATING_SYMBOL(self):
def fget(self):
return self.__output_profile.ratings_char
@ -750,7 +752,7 @@ Author '{0}':
if record['cover']:
this_title['cover'] = re.sub('&amp;', '&', record['cover'])
this_title['read'] = self.discoverReadStatus(record)
this_title['prefix'] = self.discoverPrefix(record)
if record['tags']:
this_title['tags'] = self.processSpecialTags(record['tags'],
@ -991,29 +993,17 @@ Author '{0}':
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in book.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if book['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif book['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
# Link to book
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
@ -1026,12 +1016,12 @@ Author '{0}':
else:
formatted_title = self.by_titles_normal_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
ptc += 1
spanTag.insert(stc, aTag)
stc += 1
# Dot
pBookTag.insert(ptc, NavigableString(" &middot; "))
ptc += 1
spanTag.insert(stc, NavigableString(" &middot; "))
stc += 1
# Link to author
emTag = Tag(soup, "em")
@ -1040,7 +1030,10 @@ Author '{0}':
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
aTag.insert(0, NavigableString(book['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
spanTag.insert(stc, emTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
if divRunningTag is not None:
@ -1172,10 +1165,8 @@ Author '{0}':
aTag['href'] = "%s.html#%s_series" % ('BySeries',
re.sub('\W','',book['series']).lower())
aTag.insert(0, book['series'])
#pSeriesTag.insert(0, NavigableString(self.NOT_READ_SYMBOL))
pSeriesTag.insert(0, aTag)
else:
#pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series']))
pSeriesTag.insert(0,NavigableString('%s' % book['series']))
if author_count == 1:
@ -1189,29 +1180,16 @@ Author '{0}':
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in book.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if book['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif book['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(book['id'])))
@ -1227,7 +1205,9 @@ Author '{0}':
non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
spanTag.insert(ptc, aTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
if author_count == 1:
@ -1337,29 +1317,16 @@ Author '{0}':
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in new_entry.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if new_entry['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif new_entry['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
@ -1372,7 +1339,10 @@ Author '{0}':
formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip()
non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
spanTag.insert(stc, aTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
divTag.insert(dtc, pBookTag)
@ -1393,29 +1363,16 @@ Author '{0}':
for new_entry in date_range_list:
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in new_entry.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if new_entry['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif new_entry['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
pBookTag.insert(ptc, self.formatPrefix(new_entry['prefix'],soup))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
@ -1427,12 +1384,12 @@ Author '{0}':
else:
formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
ptc += 1
spanTag.insert(stc, aTag)
stc += 1
# Dot
pBookTag.insert(ptc, NavigableString(" &middot; "))
ptc += 1
spanTag.insert(stc, NavigableString(" &middot; "))
stc += 1
# Link to author
emTag = Tag(soup, "em")
@ -1441,7 +1398,10 @@ Author '{0}':
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
spanTag.insert(stc, emTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
divTag.insert(dtc, pBookTag)
@ -1799,30 +1759,16 @@ Author '{0}':
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
book['read'] = self.discoverReadStatus(book)
book['prefix'] = self.discoverPrefix(book)
pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup))
ptc += 1
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in book.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if book.get('read', False):
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif book['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
@ -1838,12 +1784,13 @@ Author '{0}':
args = self.generateFormatArgs(book)
formatted_title = self.by_series_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
ptc += 1
spanTag.insert(stc, aTag)
stc += 1
# &middot;
pBookTag.insert(ptc, NavigableString(' &middot; '))
ptc += 1
spanTag.insert(stc, NavigableString(' &middot; '))
stc += 1
# Link to author
aTag = Tag(soup, "a")
@ -1851,7 +1798,10 @@ Author '{0}':
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
self.generateAuthorAnchor(escape(' & '.join(book['authors']))))
aTag.insert(0, NavigableString(' &amp; '.join(book['authors'])))
pBookTag.insert(ptc, aTag)
spanTag.insert(stc, aTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
divTag.insert(dtc, pBookTag)
@ -1905,7 +1855,7 @@ Author '{0}':
this_book['author'] = book['author']
this_book['title'] = book['title']
this_book['author_sort'] = capitalize(book['author_sort'])
this_book['read'] = book['read']
this_book['prefix'] = book['prefix']
this_book['tags'] = book['tags']
this_book['id'] = book['id']
this_book['series'] = book['series']
@ -3165,7 +3115,7 @@ Author '{0}':
if not os.path.isdir(images_path):
os.makedirs(images_path)
def discoverReadStatus(self, record):
def discoverPrefix(self, record):
'''
Given a field:pattern spec, discover if this book marked as read
@ -3177,25 +3127,42 @@ Author '{0}':
datatype datetime: #field_name:.*
'''
# Legacy handling of special 'read' tag
field = self.__read_book_marker['field']
pat = self.__read_book_marker['pattern']
if field == 'tag' and pat in record['tags']:
return True
if self.opts.verbose:
self.opts.log.info("\tevaluating %s (%s) for prefix matches" % (record['title'], record['authors']))
# Compare the record to each rule looking for a match
for rule in self.prefixRules:
if False and self.opts.verbose:
self.opts.log.info("\t evaluating prefix_rule '%s'" % rule['name'])
# Literal comparison for Tags field
if rule['field'].lower() == 'tags':
if rule['pattern'].lower() in map(unicode.lower,record['tags']):
if self.opts.verbose:
self.opts.log.info("\t '%s' found in '%s' (%s)" %
(rule['pattern'], rule['field'], rule['name']))
return rule['prefix']
# Regex comparison for custom field
elif rule['field'].startswith('#'):
field_contents = self.__db.get_field(record['id'],
field,
rule['field'],
index_is_id=True)
if field_contents:
try:
if re.search(pat, unicode(field_contents),
if re.search(rule['pattern'], unicode(field_contents),
re.IGNORECASE) is not None:
return True
if self.opts.verbose:
self.opts.log.info("\t '%s' found in '%s' (%s)" %
(rule['pattern'], rule['field'], rule['name']))
return rule['prefix']
except:
# Compiling of pat failed, ignore it
pass
return False
if False and self.opts.verbose:
self.opts.log.info("\t No prefix match found")
return None
def filterDbTags(self, tags):
# Remove the special marker tags from the database's tag list,
@ -3227,9 +3194,13 @@ Author '{0}':
if tag in self.markerTags:
excluded_tags.append(tag)
continue
try:
if re.search(self.opts.exclude_genre, tag):
excluded_tags.append(tag)
continue
except:
self.opts.log.error("\tfilterDbTags(): malformed --exclude-genre regex pattern: %s" % self.opts.exclude_genre)
if tag == ' ':
continue
@ -3266,6 +3237,20 @@ Author '{0}':
else:
return None
def formatPrefix(self,prefix_char,soup):
# Generate the HTML for the prefix portion of the listing
spanTag = Tag(soup, "span")
if prefix_char is None:
spanTag['style'] = "color:white"
spanTag.insert(0,NavigableString(self.defaultPrefix))
# 2e3a is 'two-em dash', which matches width in Kindle Previewer
# too wide in calibre viewer
# minimal visual distraction
# spanTag.insert(0,NavigableString(u'\u2e3a'))
else:
spanTag.insert(0,NavigableString(prefix_char))
return spanTag
def generateAuthorAnchor(self, author):
# Strip white space to ''
return re.sub("\W","", author)
@ -3359,29 +3344,16 @@ Author '{0}':
# Add books
pBookTag = Tag(soup, "p")
pBookTag['class'] = "line_item"
ptc = 0
# book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in book.get('tags', []):
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
else:
if book['read']:
# check mark
pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
elif book['id'] in self.bookmarked_books:
pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL))
pBookTag['class'] = "read_book"
ptc += 1
else:
# hidden check mark
pBookTag['class'] = "unread_book"
pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL))
pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup))
ptc += 1
spanTag = Tag(soup, "span")
spanTag['class'] = "entry"
stc = 0
# Add the book title
aTag = Tag(soup, "a")
if self.opts.generate_descriptions:
@ -3398,7 +3370,10 @@ Author '{0}':
non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag)
spanTag.insert(stc, aTag)
stc += 1
pBookTag.insert(ptc, spanTag)
ptc += 1
divTag.insert(dtc, pBookTag)
@ -3463,11 +3438,9 @@ Author '{0}':
# Author, author_prefix (read|reading|none symbol or missing symbol)
author = book['author']
if self.opts.wishlist_tag in book.get('tags', []):
author_prefix = self.MISSING_SYMBOL + " by "
else:
if book['read']:
author_prefix = self.READ_SYMBOL + " by "
if book['prefix']:
author_prefix = book['prefix'] + " by "
elif self.opts.connected_kindle and book['id'] in self.bookmarked_books:
author_prefix = self.READING_SYMBOL + " by "
else:
@ -4005,6 +3978,22 @@ Author '{0}':
return merged
def processPrefixRules(self):
# Put the prefix rules into an ordered list of dicts
try:
for rule in self.opts.prefix_rules:
prefix_rule = {}
prefix_rule['name'] = rule[0]
prefix_rule['field'] = rule[1]
prefix_rule['pattern'] = rule[2]
prefix_rule['prefix'] = rule[3]
self.prefixRules.append(prefix_rule)
except:
self.opts.log.error("malformed self.opts.prefix_rules: %s" % repr(self.opts.prefix_rules))
raise
# Use the highest order prefix symbol as default
self.defaultPrefix = self.opts.prefix_rules[0][3]
def processExclusions(self, data_set):
'''
Remove excluded entries
@ -4026,17 +4015,20 @@ Author '{0}':
return filtered_data_set
def processSpecialTags(self, tags, this_title, opts):
tag_list = []
try:
for tag in tags:
tag = self.convertHTMLEntities(tag)
if re.search(opts.exclude_genre, tag):
continue
elif self.__read_book_marker['field'] == 'tag' and \
tag == self.__read_book_marker['pattern']:
# remove 'read' tag
continue
else:
tag_list.append(tag)
except:
self.opts.log.error("\tprocessSpecialTags(): malformed --exclude-genre regex pattern: %s" % opts.exclude_genre)
return tags
return tag_list
def updateProgressFullStep(self, description):

View File

@ -719,6 +719,7 @@ def catalog_option_parser(args):
def add_plugin_parser_options(fmt, parser, log):
# Fetch the extension-specific CLI options from the plugin
# library.catalogs.<format>.py
plugin = plugin_for_catalog_format(fmt)
for option in plugin.cli_options:
if option.action:
@ -796,7 +797,6 @@ def catalog_option_parser(args):
return parser, plugin, log
def command_catalog(args, dbpath):
print("library.cli:command_catalog() EXPERIMENTAL MODE")
parser, plugin, log = catalog_option_parser(args)
opts, args = parser.parse_args(sys.argv[1:])