diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index 55a7c9fbec..40b0d0eb4e 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -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; diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 3cfd94cc6e..7d7119bbf2 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -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,'','','','')], + # ['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':'', + '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) + + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 5136f86fae..31c5063a12 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -7,7 +7,7 @@ 0 0 650 - 596 + 603 @@ -203,6 +203,9 @@ e.g., [Project Gutenberg]</p> Excluded books + + QFormLayout::FieldsStayAtSizeHint + @@ -323,96 +326,66 @@ Default: ~,Catalog - + 0 0 - - - 0 - 0 - - - - Matching books will be displayed with a check mark - - Read books + Prefix rules - - - - - QLayout::SetDefaultConstraint - + + + - - - - 175 - 0 - + + + + 0 + 0 + - 200 - 16777215 + 16777215 + 118 - - &Column/value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - read_source_field - - - - - 0 - 0 - - - - Column containing 'read' status - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 18 - - - - - - - - 150 - 0 - - - - 'read book' pattern - - - - - + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + @@ -440,49 +413,7 @@ Default: ~,Catalog QFormLayout::FieldsStayAtSizeHint - - - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - - - - &Wishlist tag - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - wishlist_tag - - - - - - - Books tagged as Wishlist items will be displayed with an X - - - - - - + @@ -542,7 +473,7 @@ Default: ~,Catalog - + @@ -599,7 +530,7 @@ Default: ~,Catalog - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 81cb247236..1e9935427c 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -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=_("#: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=_(":[before|after]:[True|False] specifying:\n" + help=_("#:[before|after]:[True|False] specifying:\n" " 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 ('','','','').\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]))) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 76f96a3397..949b1a6b81 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -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 '%s' % self.__output_profile.read_char - return property(fget=fget) - @dynamic_property def READING_SYMBOL(self): def fget(self): return '' if self.generateForKindle else \ '+' 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('&', '&', 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,28 +993,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)) - ptc += 1 + 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") @@ -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(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + 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,28 +1180,15 @@ 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)) - ptc += 1 + 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: @@ -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,28 +1317,15 @@ 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)) - ptc += 1 + 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: @@ -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,28 +1363,15 @@ 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)) - ptc += 1 + 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: @@ -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(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + 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 # · - pBookTag.insert(ptc, NavigableString(' · ')) - ptc += 1 + spanTag.insert(stc, NavigableString(' · ')) + 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(' & '.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']) - field_contents = self.__db.get_field(record['id'], - field, + # 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'], + rule['field'], index_is_id=True) - if field_contents: - try: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - return True - except: - # Compiling of pat failed, ignore it - pass + if field_contents: + try: + if re.search(rule['pattern'], unicode(field_contents), + re.IGNORECASE) is not None: + 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 + + if False and self.opts.verbose: + self.opts.log.info("\t No prefix match found") + return None - return False 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 - if re.search(self.opts.exclude_genre, tag): - 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,28 +3344,15 @@ 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)) - ptc += 1 + 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") @@ -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,15 +3438,13 @@ 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 " + + 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: - if book['read']: - author_prefix = self.READ_SYMBOL + " by " - elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: - author_prefix = self.READING_SYMBOL + " by " - else: - author_prefix = "by " + author_prefix = "by " # Genres genres = '' @@ -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 = [] - 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) + + try: + for tag in tags: + tag = self.convertHTMLEntities(tag) + if re.search(opts.exclude_genre, 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): diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 4fc268ce7d..2b65ed37da 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -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..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:])