diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css
index 057c6c9f42..bf83a4c60b 100644
--- a/resources/catalog/stylesheet.css
+++ b/resources/catalog/stylesheet.css
@@ -2,19 +2,29 @@ body { background-color: white; }
p.title {
margin-top:0em;
- margin-bottom:1em;
+ margin-bottom:0em;
text-align:center;
font-style:italic;
font-size:xx-large;
- border-bottom: solid black 2px;
+ }
+
+p.series_id {
+ margin-top:0em;
+ margin-bottom:0em;
+ text-align:center;
+ }
+
+a.series_id {
+ font-style:normal;
+ font-size:large;
}
p.author {
+ font-size:large;
margin-top:0em;
margin-bottom:0em;
text-align: center;
text-indent: 0em;
- font-size:large;
}
p.author_index {
@@ -26,7 +36,8 @@ p.author_index {
text-indent: 0em;
}
-p.tags {
+p.genres {
+ font-style:normal;
margin-top:0.5em;
margin-bottom:0em;
text-align: left;
@@ -108,6 +119,13 @@ p.date_read {
text-indent:-6em;
}
+hr.annotations_divider {
+ width:50%;
+ margin-left:1em;
+ margin-top:0em;
+ margin-bottom:0em;
+ }
+
hr.description_divider {
width:90%;
margin-left:5%;
@@ -117,20 +135,37 @@ hr.description_divider {
border-left: solid white 0px;
}
-hr.annotations_divider {
- width:50%;
- margin-left:1em;
- margin-top:0em;
- margin-bottom:0em;
+hr.header_divider {
+ width:100%;
+ border-top: solid white 1px;
+ border-right: solid white 0px;
+ border-bottom: solid black 2px;
+ border-left: solid white 0px;
+ }
+
+hr.merged_comments_divider {
+ width:80%;
+ margin-left:10%;
+ border-top: solid white 0px;
+ border-right: solid white 0px;
+ border-bottom: dashed gray 2px;
+ border-left: solid white 0px;
}
td.publisher, td.date {
font-weight:bold;
text-align:center;
}
-td.rating {
- text-align: center;
+
+td.rating{
+ text-align:center;
}
+
+td.notes {
+ font-size: 100%;
+ text-align:center;
+ }
+
td.thumbnail img {
-webkit-box-shadow: 4px 4px 12px #999;
}
\ No newline at end of file
diff --git a/resources/catalog/template.xhtml b/resources/catalog/template.xhtml
new file mode 100644
index 0000000000..409086d343
--- /dev/null
+++ b/resources/catalog/template.xhtml
@@ -0,0 +1,41 @@
+
+
+ {title_str}
+
+
+
+
+ {title}
+ {series} [{series_index}]
+
+ {author_prefix}{author}
+ {genres}
+ {formats}
+
+
+ |
+ |
+
+
+ |
+
+
+ {publisher} |
+
+
+ {pubyear} |
+
+
+ {rating} |
+
+
+ {note_source}: {note_content} |
+
+
+ |
+
+
+
+
+
+
diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py
index a76cb71acd..770d405203 100644
--- a/src/calibre/customize/__init__.py
+++ b/src/calibre/customize/__init__.py
@@ -307,6 +307,14 @@ class CatalogPlugin(Plugin): # {{{
#: cli_options parsed in library.cli:catalog_option_parser()
cli_options = []
+ def _field_sorter(self, key):
+ '''
+ Custom fields sort after standard fields
+ '''
+ if key.startswith('#'):
+ return '~%s' % key[1:]
+ else:
+ return key
def search_sort_db(self, db, opts):
@@ -315,18 +323,18 @@ class CatalogPlugin(Plugin): # {{{
if opts.sort_by:
# 2nd arg = ascending
db.sort(opts.sort_by, True)
-
return db.get_data_as_dict(ids=opts.ids)
- def get_output_fields(self, opts):
+ def get_output_fields(self, db, opts):
# Return a list of requested fields, with opts.sort_by first
- all_fields = set(
+ all_std_fields = set(
['author_sort','authors','comments','cover','formats',
'id','isbn','ondevice','pubdate','publisher','rating',
'series_index','series','size','tags','timestamp',
'title','uuid'])
+ all_custom_fields = set(db.custom_field_keys())
+ all_fields = all_std_fields.union(all_custom_fields)
- fields = all_fields
if opts.fields != 'all':
# Make a list from opts.fields
requested_fields = set(opts.fields.split(','))
@@ -337,7 +345,7 @@ class CatalogPlugin(Plugin): # {{{
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
fields.pop(int(fields.index('ondevice')))
- fields.sort()
+ fields = sorted(fields, key=self._field_sorter)
if opts.sort_by and opts.sort_by in fields:
fields.insert(0,fields.pop(int(fields.index(opts.sort_by))))
return fields
diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py
index 177f482aa6..0c27069df3 100644
--- a/src/calibre/customize/profiles.py
+++ b/src/calibre/customize/profiles.py
@@ -624,6 +624,8 @@ class KindleDXOutput(OutputProfile):
#comic_screen_size = (741, 1022)
supports_mobi_indexing = True
periodical_date_in_title = False
+ missing_char = u'x\u2009'
+ empty_ratings_char = u'\u2606'
ratings_char = u'\u2605'
read_char = u'\u2713'
mobi_ems_per_blockquote = 2.0
diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py
index a253664a1e..0eba0406a1 100644
--- a/src/calibre/gui2/actions/catalog.py
+++ b/src/calibre/gui2/actions/catalog.py
@@ -57,7 +57,7 @@ class GenerateCatalogAction(InterfaceAction):
if job.result:
# Search terms nulled catalog results
return error_dialog(self.gui, _('No books found'),
- _("No books to catalog\nCheck exclude tags"),
+ _("No books to catalog\nCheck exclusion criteria"),
show=True)
if job.failed:
return self.gui.job_exception(job)
diff --git a/src/calibre/gui2/catalog/catalog_csv_xml.py b/src/calibre/gui2/catalog/catalog_csv_xml.py
index 077d4cbbca..18f2c210dc 100644
--- a/src/calibre/gui2/catalog/catalog_csv_xml.py
+++ b/src/calibre/gui2/catalog/catalog_csv_xml.py
@@ -6,9 +6,9 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-
from calibre.gui2 import gprefs
from calibre.gui2.catalog.catalog_csv_xml_ui import Ui_Form
+from calibre.library import db as db_
from PyQt4.Qt import QWidget, QListWidgetItem
class PluginWidget(QWidget, Ui_Form):
@@ -28,6 +28,12 @@ class PluginWidget(QWidget, Ui_Form):
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)
+ db = db_()
+ for x in sorted(db.custom_field_keys()):
+ self.all_fields.append(x)
+ QListWidgetItem(x, self.db_fields)
+
+
def initialize(self, name, db):
self.name = name
fields = gprefs.get(name+'_db_fields', self.all_fields)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index 1ae4efd014..7a35fdb3c2 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -17,18 +17,55 @@ class PluginWidget(QWidget,Ui_Form):
TITLE = _('E-book options')
HELP = _('Options specific to')+' EPUB/MOBI '+_('output')
- OPTION_FIELDS = [('exclude_genre','\[.+\]'),
- ('exclude_tags','~,'+_('Catalog')),
- ('generate_titles', True),
- ('generate_series', True),
- ('generate_recently_added', True),
- ('note_tag','*'),
- ('numbers_as_text', False),
- ('read_pattern','+'),
- ('read_source_field_cb','Tag'),
- ('wishlist_tag','Wishlist'),
- ]
+ CheckBoxControls = [
+ 'generate_titles',
+ 'generate_series',
+ 'generate_genres',
+ 'generate_recently_added',
+ 'generate_descriptions',
+ 'include_hr'
+ ]
+ ComboBoxControls = [
+ 'read_source_field',
+ 'exclude_source_field',
+ 'header_note_source_field',
+ 'merge_source_field'
+ ]
+ LineEditControls = [
+ 'exclude_genre',
+ 'exclude_pattern',
+ 'exclude_tags',
+ 'read_pattern',
+ 'wishlist_tag'
+ ]
+ RadioButtonControls = [
+ 'merge_before',
+ 'merge_after'
+ ]
+ SpinBoxControls = [
+ 'thumb_width'
+ ]
+
+ OPTION_FIELDS = zip(CheckBoxControls,
+ [True for i in CheckBoxControls],
+ ['check_box' for i in CheckBoxControls])
+ OPTION_FIELDS += zip(ComboBoxControls,
+ [None for i in ComboBoxControls],
+ ['combo_box' for i in ComboBoxControls])
+ OPTION_FIELDS += zip(RadioButtonControls,
+ [None for i in RadioButtonControls],
+ ['radio_button' for i in RadioButtonControls])
+
+ # LineEditControls
+ 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'])
# Output synced to the connected device?
sync_enabled = True
@@ -42,105 +79,203 @@ class PluginWidget(QWidget,Ui_Form):
def initialize(self, name, db):
self.name = name
-
- # Populate the 'Read book' source fields
- all_custom_fields = db.custom_field_keys()
- custom_fields = {}
- custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
- for custom_field in all_custom_fields:
- field_md = db.metadata_for_field(custom_field)
- if field_md['datatype'] in ['bool','composite','datetime','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_cb.addItem(cf)
-
- self.read_source_fields = custom_fields
- self.read_source_field_cb.currentIndexChanged.connect(self.read_source_field_changed)
+ self.db = db
+ self.populateComboBoxes()
# Update dialog fields from stored options
for opt in self.OPTION_FIELDS:
- opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
- if opt[0] in [
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- getattr(self, opt[0]).setChecked(opt_value)
+ c_name, c_def, c_type = opt
+ opt_value = gprefs.get(self.name + '_' + c_name, c_def)
+ if c_type in ['check_box']:
+ getattr(self, c_name).setChecked(eval(str(opt_value)))
+ elif c_type in ['combo_box'] and opt_value is not None:
+ # *** Test this code with combo boxes ***
+ #index = self.read_source_field.findText(opt_value)
+ index = getattr(self,c_name).findText(opt_value)
+ if index == -1 and c_name == 'read_source_field':
+ index = self.read_source_field.findText('Tag')
+ #self.read_source_field.setCurrentIndex(index)
+ 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))
- # Combo box
- elif opt[0] in ['read_source_field_cb']:
- # Look for last-stored combo box value
- index = self.read_source_field_cb.findText(opt_value)
- if index == -1:
- index = self.read_source_field_cb.findText('Tag')
- self.read_source_field_cb.setCurrentIndex(index)
-
- # Text fields
- else:
- getattr(self, opt[0]).setText(opt_value)
-
- # Init self.read_source_field
- cs = unicode(self.read_source_field_cb.currentText())
+ # 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 = read_source_spec['field']
+ self.read_source_field_name = read_source_spec['field']
+
+ # Init self.exclude_source_field_name
+ self.exclude_source_field_name = ''
+ cs = unicode(self.exclude_source_field.currentText())
+ if cs > '':
+ exclude_source_spec = self.exclude_source_fields[cs]
+ self.exclude_source_field_name = exclude_source_spec['field']
+
+ # Init self.merge_source_field_name
+ self.merge_source_field_name = ''
+ cs = unicode(self.merge_source_field.currentText())
+ if cs > '':
+ merge_source_spec = self.merge_source_fields[cs]
+ self.merge_source_field_name = merge_source_spec['field']
+
+ # Init self.header_note_source_field_name
+ self.header_note_source_field_name = ''
+ cs = unicode(self.header_note_source_field.currentText())
+ if cs > '':
+ header_note_source_spec = self.header_note_source_fields[cs]
+ self.header_note_source_field_name = header_note_source_spec['field']
+
+ # Hook changes to thumb_width
+ self.thumb_width.valueChanged.connect(self.thumb_width_changed)
def options(self):
# Save/return the current options
# exclude_genre stores literally
# generate_titles, generate_recently_added, numbers_as_text stores as True/False
# others store as lists
+
opts_dict = {}
+ # Save values to gprefs
for opt in self.OPTION_FIELDS:
- # Save values to gprefs
- if opt[0] in [
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- opt_value = getattr(self,opt[0]).isChecked()
+ c_name, c_def, c_type = opt
+ if c_type in ['check_box', 'radio_button']:
+ opt_value = getattr(self, c_name).isChecked()
+ elif c_type in ['combo_box']:
+ 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())
+ gprefs.set(self.name + '_' + c_name, opt_value)
- # Combo box uses .currentText()
- elif opt[0] in ['read_source_field_cb']:
- opt_value = unicode(getattr(self, opt[0]).currentText())
-
- # text fields use .text()
+ # Construct opts object
+ if c_name == 'exclude_tags':
+ # store as list
+ opts_dict[c_name] = opt_value.split(',')
else:
- opt_value = unicode(getattr(self, opt[0]).text())
- gprefs.set(self.name + '_' + opt[0], opt_value)
+ opts_dict[c_name] = opt_value
- # Construct opts
- if opt[0] in [
- 'exclude_genre',
- 'generate_recently_added',
- 'generate_series',
- 'generate_titles',
- 'numbers_as_text',
- ]:
- opts_dict[opt[0]] = opt_value
- else:
- opts_dict[opt[0]] = opt_value.split(',')
+ # Generate markers for hybrids
+ opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name,
+ self.read_pattern.text())
+ opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name,
+ self.exclude_pattern.text())
- # Generate read_book_marker
- opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field, self.read_pattern.text())
+ # Generate specs for merge_comments, header_note_source_field
+ checked = ''
+ if self.merge_before.isChecked():
+ checked = 'before'
+ elif self.merge_after.isChecked():
+ checked = 'after'
+ include_hr = self.include_hr.isChecked()
+ opts_dict['merge_comments'] = "%s:%s:%s" % \
+ (self.merge_source_field_name, checked, include_hr)
+
+ opts_dict['header_note_source_field'] = self.header_note_source_field_name
# Append the output profile
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
+ if False:
+ print "opts_dict"
+ for opt in sorted(opts_dict.keys()):
+ print " %s: %s" % (opt, repr(opts_dict[opt]))
return opts_dict
+ def populateComboBoxes(self):
+ # Custom column types declared in
+ # gui2.preferences.create_custom_column:CreateCustomColumn()
+ # As of 0.7.34:
+ # bool Yes/No
+ # comments Long text, like comments, not shown in tag browser
+ # composite Column built from other columns
+ # datetime Date
+ # enumeration Text, but with a fixed set of permitted values
+ # float Floating point numbers
+ # int Integers
+ # rating Ratings, shown with stars
+ # series Text column for keeping series-like information
+ # 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:
+ 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']}
+ # Blank field first
+ self.exclude_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.exclude_source_field.addItem(cf)
+ self.exclude_source_fields = custom_fields
+ self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed)
+
+
+ # Populate the 'Header note' combo box
+ custom_fields = {}
+ 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']}
+ # Blank field first
+ self.header_note_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.header_note_source_field.addItem(cf)
+ self.header_note_source_fields = custom_fields
+ self.header_note_source_field.currentIndexChanged.connect(self.header_note_source_field_changed)
+
+
+ # Populate the 'Merge with Comments' combo box
+ custom_fields = {}
+ for custom_field in all_custom_fields:
+ field_md = self.db.metadata_for_field(custom_field)
+ if field_md['datatype'] in ['text','comments']:
+ custom_fields[field_md['name']] = {'field':custom_field,
+ 'datatype':field_md['datatype']}
+ # Blank field first
+ self.merge_source_field.addItem('')
+ # Add the sorted eligible fields to the combo box
+ for cf in sorted(custom_fields):
+ self.merge_source_field.addItem(cf)
+ self.merge_source_fields = custom_fields
+ self.merge_source_field.currentIndexChanged.connect(self.merge_source_field_changed)
+ self.merge_before.setEnabled(False)
+ self.merge_after.setEnabled(False)
+ self.include_hr.setEnabled(False)
+
def read_source_field_changed(self,new_index):
'''
Process changes in the read_source_field combo box
Currently using QLineEdit for all field types
Possible to modify to switch QWidget type
'''
- new_source = str(self.read_source_field_cb.currentText())
+ new_source = str(self.read_source_field.currentText())
read_source_spec = self.read_source_fields[str(new_source)]
- self.read_source_field = read_source_spec['field']
+ self.read_source_field_name = read_source_spec['field']
# Change pattern input widget to match the source field datatype
if read_source_spec['datatype'] in ['bool','composite','datetime','text']:
@@ -152,3 +287,63 @@ class PluginWidget(QWidget,Ui_Form):
self.read_pattern = dw
self.read_spec_hl.addWidget(dw)
+ def exclude_source_field_changed(self,new_index):
+ '''
+ Process changes in the exclude_source_field combo box
+ Currently using QLineEdit for all field types
+ Possible to modify to switch QWidget type
+ '''
+ new_source = str(self.exclude_source_field.currentText())
+ self.exclude_source_field_name = new_source
+ if new_source > '':
+ exclude_source_spec = self.exclude_source_fields[str(new_source)]
+ self.exclude_source_field_name = exclude_source_spec['field']
+ self.exclude_pattern.setEnabled(True)
+
+ # Change pattern input widget to match the source field datatype
+ if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']:
+ if not isinstance(self.exclude_pattern, QLineEdit):
+ self.exclude_spec_hl.removeWidget(self.exclude_pattern)
+ dw = QLineEdit(self)
+ dw.setObjectName('exclude_pattern')
+ dw.setToolTip('Exclusion pattern')
+ self.exclude_pattern = dw
+ self.exclude_spec_hl.addWidget(dw)
+ else:
+ self.exclude_pattern.setEnabled(False)
+
+ def header_note_source_field_changed(self,new_index):
+ '''
+ Process changes in the header_note_source_field combo box
+ '''
+ new_source = str(self.header_note_source_field.currentText())
+ self.header_note_source_field_name = new_source
+ if new_source > '':
+ header_note_source_spec = self.header_note_source_fields[str(new_source)]
+ self.header_note_source_field_name = header_note_source_spec['field']
+
+ def merge_source_field_changed(self,new_index):
+ '''
+ Process changes in the header_note_source_field combo box
+ '''
+ new_source = str(self.merge_source_field.currentText())
+ self.merge_source_field_name = new_source
+ if new_source > '':
+ merge_source_spec = self.merge_source_fields[str(new_source)]
+ self.merge_source_field_name = merge_source_spec['field']
+ if not self.merge_before.isChecked() and not self.merge_after.isChecked():
+ self.merge_after.setChecked(True)
+ self.merge_before.setEnabled(True)
+ self.merge_after.setEnabled(True)
+ self.include_hr.setEnabled(True)
+
+ else:
+ self.merge_before.setEnabled(False)
+ self.merge_after.setEnabled(False)
+ self.include_hr.setEnabled(False)
+
+ def thumb_width_changed(self,new_value):
+ '''
+ Process changes in the thumb_width spin box
+ '''
+ pass
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index d72566f581..7a2a86c690 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -6,163 +6,681 @@
0
0
- 627
- 549
+ 650
+ 582
+
+
+ 0
+ 0
+
+
Form
-
- -
-
-
- 'Don't include this book' tag:
+
+
-
+
+
+
+ 0
+ 0
+
-
-
- -
-
-
-
-
-
-
- -
-
-
- Additional note tag prefix:
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- Regex pattern describing tags to exclude as genres:
-
-
- Qt::LogText
-
-
- true
-
-
-
- -
-
-
- Regex tips:
-- The default regex - \[.+\] - excludes genre tags of the form [tag], e.g., [Amazon Freebie]
-- A regex pattern of a single dot excludes all genre tags, generating no Genre Section
-
-
- true
-
-
-
- -
-
-
- Qt::Vertical
-
-
+
- 20
- 40
+ 0
+ 0
-
-
- -
-
-
- Include 'Titles' Section
+
+ Sections to include in catalog. All catalogs include 'Books by Author'.
+
+ Included sections
+
+
+
-
+
+
+ Books by &Title
+
+
+
+ -
+
+
+ Books by &Series
+
+
+
+ -
+
+
+ Recently &Added
+
+
+
+ -
+
+
+ Books by &Genre
+
+
+
+ -
+
+
+ &Descriptions
+
+
+
+
- -
-
-
- Include 'Recently Added' Section
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern </p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Courier New,courier';">\[.+\]</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">excludes tags of the form [<span style=" font-family:'Courier New,courier';">tag</span>], </p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">e.g., [Project Gutenberg]</p></body></html>
+
+
+ Excluded genres
+
+
+
+ QFormLayout::FieldsStayAtSizeHint
+
+
-
+
+
+ -1
+
+
+ 0
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Tags to &exclude
+
+
+ Qt::AutoText
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+ exclude_genre
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
- -
-
-
- Sort numbers as text
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ Books matching either pattern will not be included in generated catalog.
+
+
+ Excluded books
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Tags to &exclude
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+ exclude_tags
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">Comma-separated list of tags to exclude.</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">Default:</span><span style=" font-family:'Courier New,courier'; font-size:12pt;"> ~,Catalog</span></p></body></html>
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ &Column/value
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+ exclude_source_field
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Column containing additional exclusion criteria
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+ 18
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+ Exclusion pattern
+
+
+
+
+
+
- -
-
-
- Include 'Series' Section
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ Matching books will be displayed with ✓
+
+
+ Read books
+
+
+
-
+
+
+ QLayout::SetDefaultConstraint
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ &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
+
+
+
+
+
+
+
+
+
- -
-
-
- -
-
-
- Wishlist tag:
+
-
+
+
+
+ 0
+ 0
+
-
-
- -
-
-
- QLayout::SetMinimumSize
+
+
+ 0
+ 0
+
-
-
-
-
-
- 0
- 0
-
-
-
- Source column for read book
-
-
-
-
-
-
- -
-
-
- Pattern for read book
-
-
-
-
-
-
-
-
- -
-
-
- Books marked as read:
+
+ Other options
+
+
+ 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 ✕
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ &Thumbnail width
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ true
+
+
+ thumb_width
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Size hint for Description cover thumbnails
+
+
+ inch
+
+
+ 2
+
+
+ 1.000000000000000
+
+
+ 2.000000000000000
+
+
+ 0.100000000000000
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+
+
+
+ &Description note
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ header_note_source_field
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Custom column source for note to include in Description header area
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 175
+ 0
+
+
+
+
+ 200
+ 16777215
+
+
+
+ &Merge with Comments
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ merge_source_field
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Additional content merged with Comments during catalog generation
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Merge additional content before Comments
+
+
+ &Before
+
+
+
+ -
+
+
+ Merge additional content after Comments
+
+
+ &After
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Separate Comments and additional content with horizontal rule
+
+
+ &Separator
+
+
+
+
+
+
diff --git a/src/calibre/gui2/catalog/catalog_tab_template.ui b/src/calibre/gui2/catalog/catalog_tab_template.ui
index 5df881beac..4b24507f80 100644
--- a/src/calibre/gui2/catalog/catalog_tab_template.ui
+++ b/src/calibre/gui2/catalog/catalog_tab_template.ui
@@ -6,8 +6,8 @@
0
0
- 579
- 411
+ 650
+ 575
diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui
index e1de9407ea..62ac7cb5af 100644
--- a/src/calibre/gui2/dialogs/catalog.ui
+++ b/src/calibre/gui2/dialogs/catalog.ui
@@ -6,8 +6,8 @@
0
0
- 611
- 514
+ 674
+ 660
@@ -33,6 +33,18 @@
-
+
+
+ 0
+ 0
+
+
+
+
+ 650
+ 575
+
+
0
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 0b317d6a6e..5f771a8a6d 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -1,22 +1,24 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
-__copyright__ = '2010, Greg Riker '
+__copyright__ = '2010, Greg Riker'
import codecs, datetime, htmlentitydefs, os, re, shutil, time, zlib
from contextlib import closing
from collections import namedtuple
from copy import deepcopy
from xml.sax.saxutils import escape
+from lxml import etree
from calibre import prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding
from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
+from calibre.ebooks.oeb.base import RECOVER_PARSER, XHTML_NS
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config import config_dir
-from calibre.utils.date import isoformat, now as nowf
+from calibre.utils.date import format_date, isoformat, now as nowf
from calibre.utils.logging import default_log as log
from calibre.utils.zipfile import ZipFile, ZipInfo
from calibre.utils.magick.draw import thumbnail
@@ -26,6 +28,7 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
+
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
@@ -49,7 +52,8 @@ class CSV_XML(CatalogPlugin):
action = None,
help = _('The fields to output when cataloging books in the '
'database. Should be a comma-separated list of fields.\n'
- 'Available fields: %s.\n'
+ 'Available fields: %s,\n'
+ 'plus user-created custom fields.\n'
"Default: '%%default'\n"
"Applies to: CSV, XML output formats")%', '.join(FIELDS)),
@@ -96,7 +100,7 @@ class CSV_XML(CatalogPlugin):
#raise SystemExit(1)
# Get the requested output fields as a list
- fields = self.get_output_fields(opts)
+ fields = self.get_output_fields(db, opts)
# If connected device, add 'On Device' values to data
if opts.connected_device['is_device_connected'] and 'ondevice' in fields:
@@ -106,6 +110,9 @@ class CSV_XML(CatalogPlugin):
if self.fmt == 'csv':
outfile = codecs.open(path_to_output, 'w', 'utf8')
+ # Write a UTF-8 BOM
+ outfile.write('\xef\xbb\xbf')
+
# Output the field headers
outfile.write(u'%s\n' % u','.join(fields))
@@ -113,7 +120,10 @@ class CSV_XML(CatalogPlugin):
for entry in data:
outstr = []
for field in fields:
- item = entry[field]
+ if field.startswith('#'):
+ item = db.get_field(entry['id'],field,index_is_id=True)
+ else:
+ item = entry[field]
if item is None:
outstr.append('""')
continue
@@ -132,14 +142,12 @@ class CSV_XML(CatalogPlugin):
elif field == 'comments':
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
-
outstr.append(u'"%s"' % unicode(item).replace('"','""'))
outfile.write(u','.join(outstr) + u'\n')
outfile.close()
elif self.fmt == 'xml':
- from lxml import etree
from lxml.builder import E
root = E.calibredb()
@@ -147,6 +155,14 @@ class CSV_XML(CatalogPlugin):
record = E.record()
root.append(record)
+ for field in fields:
+ if field.startswith('#'):
+ val = db.get_field(r['id'],field,index_is_id=True)
+ if not isinstance(val, (str, unicode)):
+ val = unicode(val)
+ item = getattr(E, field.replace('#','_'))(val)
+ record.append(item)
+
for field in ('id', 'uuid', 'title', 'publisher', 'rating', 'size',
'isbn','ondevice'):
if field in fields:
@@ -468,7 +484,7 @@ class BIBTEX(CatalogPlugin):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
# Get the requested output fields as a list
- fields = self.get_output_fields(opts)
+ fields = self.get_output_fields(db, opts)
if not len(data):
log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
@@ -533,6 +549,20 @@ class EPUB_MOBI(CatalogPlugin):
version = (0, 0, 1)
file_types = set(['epub','mobi'])
+ THUMB_SMALLEST = "1.0"
+ THUMB_LARGEST = "2.0"
+
+ '''
+ # Deprecated, keeping this just in case there are complaints
+ Option('--numbers-as-text',
+ default=False,
+ dest='numbers_as_text',
+ action = None,
+ help=_("Sort titles with leading numbers as text, e.g.,\n'2001: A Space Odyssey' sorts as \n'Two Thousand One: A Space Odyssey'.\n"
+ "Default: '%default'\n"
+ "Applies to: ePub, MOBI output formats")),
+ '''
+
cli_options = [Option('--catalog-title',
default = 'My Books',
dest = 'catalog_title',
@@ -550,6 +580,13 @@ class EPUB_MOBI(CatalogPlugin):
"of the conversion process a bug is occurring.\n"
"Default: '%default'None\n"
"Applies to: ePub, MOBI output formats")),
+ Option('--exclude-book-marker',
+ default=':',
+ dest='exclude_book_marker',
+ action = None,
+ help=_("field:pattern specifying custom field/contents indicating book should be excluded.\n"
+ "Default: '%default'\n"
+ "Applies to ePub, MOBI output formats")),
Option('--exclude-genre',
default='\[.+\]',
dest='exclude_genre',
@@ -564,41 +601,58 @@ class EPUB_MOBI(CatalogPlugin):
"--exclude-tags=skip will match 'skip this book' and 'Skip will like this'.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
+ Option('--generate-descriptions',
+ default=True,
+ dest='generate_descriptions',
+ action = 'store_true',
+ help=_("Include book descriptions in catalog.\n"
+ "Default: '%default'\n"
+ "Applies to: ePub, MOBI output formats")),
+ Option('--generate-genres',
+ default=True,
+ dest='generate_genres',
+ action = 'store_true',
+ help=_("Include 'Genres' section in catalog.\n"
+ "Default: '%default'\n"
+ "Applies to: ePub, MOBI output formats")),
Option('--generate-titles',
- default=False,
+ default=True,
dest='generate_titles',
action = 'store_true',
help=_("Include 'Titles' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-series',
- default=False,
+ default=True,
dest='generate_series',
action = 'store_true',
help=_("Include 'Series' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-recently-added',
- default=False,
+ default=True,
dest='generate_recently_added',
action = 'store_true',
help=_("Include 'Recently Added' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
- Option('--note-tag',
- default='*',
- dest='note_tag',
+ Option('--header-note-source-field',
+ default='',
+ dest='header_note_source_field',
action = None,
- help=_("Tag prefix for user notes, e.g. '*Jeff might enjoy reading this'.\n"
+ help=_("Custom field containing note text to insert in Description header.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
- Option('--numbers-as-text',
- default=False,
- dest='numbers_as_text',
+ Option('--merge-comments',
+ default='::',
+ dest='merge_comments',
action = None,
- help=_("Sort titles with leading numbers as text, e.g.,\n'2001: A Space Odyssey' sorts as \n'Two Thousand One: A Space Odyssey'.\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"
"Default: '%default'\n"
- "Applies to: ePub, MOBI output formats")),
+ "Applies to ePub, MOBI output formats")),
Option('--output-profile',
default=None,
dest='output_profile',
@@ -612,6 +666,14 @@ class EPUB_MOBI(CatalogPlugin):
action = None,
help=_("field:pattern indicating book has been read.\n" "Default: '%default'\n"
"Applies to ePub, MOBI output formats")),
+ Option('--thumb-width',
+ default='1.0',
+ dest='thumb_width',
+ action = None,
+ help=_("Size hint (in inches) for book covers in catalog.\n"
+ "Range: 1.0 - 2.0\n"
+ "Default: '%default'\n"
+ "Applies to ePub, MOBI output formats")),
Option('--wishlist-tag',
default='Wishlist',
dest='wishlist_tag',
@@ -845,6 +907,7 @@ class EPUB_MOBI(CatalogPlugin):
catalog.copyResources()
catalog.buildSources()
'''
+
# A single number creates 'Last x days' only.
# Multiple numbers create 'Last x days', 'x to y days ago' ...
# e.g, [7,15,30,60], [30]
@@ -889,8 +952,10 @@ class EPUB_MOBI(CatalogPlugin):
and self.generateForKindle \
else False
self.__genres = None
+ self.genres = []
self.__genre_tags_dict = None
- self.__htmlFileList = []
+ self.__htmlFileList_1 = []
+ self.__htmlFileList_2 = []
self.__markerTags = self.getMarkerTags()
self.__ncxSoup = None
self.__output_profile = None
@@ -900,13 +965,15 @@ class EPUB_MOBI(CatalogPlugin):
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
self.__stylesheet = stylesheet
self.__thumbs = None
self.__thumbWidth = 0
self.__thumbHeight = 0
self.__title = opts.catalog_title
- self.__totalSteps = 11.0
+ self.__totalSteps = 8.0
self.__useSeriesPrefixInTitlesSection = False
self.__verbose = opts.verbose
@@ -916,17 +983,35 @@ class EPUB_MOBI(CatalogPlugin):
self.__output_profile = profile
break
- # Confirm/create thumbs archive
- if not os.path.exists(self.__cache_dir):
- self.opts.log.info(" creating new thumb cache '%s'" % self.__cache_dir)
- os.makedirs(self.__cache_dir)
- if not os.path.exists(self.__archive_path):
- self.opts.log.info(" creating thumbnail archive")
- zfw = ZipFile(self.__archive_path, mode='w')
- zfw.writestr("Catalog Thumbs Archive",'')
- zfw.close()
- else:
- self.opts.log.info(" existing thumb cache at '%s'" % self.__archive_path)
+ # Confirm/create thumbs archive.
+ if self.opts.generate_descriptions:
+ if not os.path.exists(self.__cache_dir):
+ self.opts.log.info(" creating new thumb cache '%s'" % self.__cache_dir)
+ os.makedirs(self.__cache_dir)
+ if not os.path.exists(self.__archive_path):
+ self.opts.log.info(' creating thumbnail archive, thumb_width: %1.2f"' %
+ float(self.opts.thumb_width))
+ zfw = ZipFile(self.__archive_path, mode='w')
+ zfw.writestr("Catalog Thumbs Archive",'')
+ #zfw.comment = "thumb_width: %1.2f" % float(self.opts.thumb_width)
+ zfw.close()
+ else:
+ with closing(ZipFile(self.__archive_path, mode='r')) as zfr:
+ try:
+ cached_thumb_width = zfr.read('thumb_width')
+ except:
+ cached_thumb_width = "-1"
+
+ if float(cached_thumb_width) != float(self.opts.thumb_width):
+ self.opts.log.warning(" invalidating cache at '%s'" % self.__archive_path)
+ self.opts.log.warning(' thumb_width changed: %1.2f" => %1.2f"' %
+ (float(cached_thumb_width),float(self.opts.thumb_width)))
+ os.remove(self.__archive_path)
+ with closing(ZipFile(self.__archive_path, mode='w')) as zfw:
+ zfw.writestr("Catalog Thumbs Archive",'')
+ else:
+ self.opts.log.info(' existing thumb cache at %s, cached_thumb_width: %1.2f"' %
+ (self.__archive_path, float(cached_thumb_width)))
# Tweak build steps based on optional sections: 1 call for HTML, 1 for NCX
if self.opts.generate_titles:
@@ -937,6 +1022,9 @@ class EPUB_MOBI(CatalogPlugin):
self.__totalSteps += 2
if self.opts.generate_series:
self.__totalSteps += 2
+ if self.opts.generate_descriptions:
+ # +1 thumbs
+ self.__totalSteps += 3
# Accessors
if True:
@@ -1079,11 +1167,18 @@ class EPUB_MOBI(CatalogPlugin):
self.__genre_tags_dict = val
return property(fget=fget, fset=fset)
@dynamic_property
- def htmlFileList(self):
+ def htmlFileList_1(self):
def fget(self):
- return self.__htmlFileList
+ return self.__htmlFileList_1
def fset(self, val):
- self.__htmlFileList = val
+ self.__htmlFileList_1 = val
+ return property(fget=fget, fset=fset)
+ @dynamic_property
+ def htmlFileList_2(self):
+ def fget(self):
+ return self.__htmlFileList_2
+ def fset(self, val):
+ self.__htmlFileList_2 = val
return property(fget=fget, fset=fset)
@dynamic_property
def libraryPath(self):
@@ -1246,20 +1341,21 @@ class EPUB_MOBI(CatalogPlugin):
return False
self.fetchBooksByAuthor()
self.fetchBookmarks()
- self.generateHTMLDescriptions()
+ if self.opts.generate_descriptions:
+ self.generateHTMLDescriptions()
self.generateHTMLByAuthor()
if self.opts.generate_titles:
self.generateHTMLByTitle()
if self.opts.generate_series:
self.generateHTMLBySeries()
+ if self.opts.generate_genres:
+ self.generateHTMLByTags()
if self.opts.generate_recently_added:
self.generateHTMLByDateAdded()
if self.generateRecentlyRead:
self.generateHTMLByDateRead()
- self.generateHTMLByTags()
-
- self.generateThumbnails()
-
+ if self.opts.generate_descriptions:
+ self.generateThumbnails()
self.generateOPF()
self.generateNCXHeader()
self.generateNCXByAuthor("Authors")
@@ -1267,12 +1363,15 @@ class EPUB_MOBI(CatalogPlugin):
self.generateNCXByTitle("Titles")
if self.opts.generate_series:
self.generateNCXBySeries("Series")
+ if self.opts.generate_genres:
+ self.generateNCXByGenre("Genres")
if self.opts.generate_recently_added:
self.generateNCXByDateAdded("Recently Added")
if self.generateRecentlyRead:
self.generateNCXByDateRead("Recently Read")
- self.generateNCXByGenre("Genres")
- self.generateNCXDescriptions("Descriptions")
+ if self.opts.generate_descriptions:
+ self.generateNCXDescriptions("Descriptions")
+
self.writeNCX()
return True
@@ -1340,10 +1439,13 @@ class EPUB_MOBI(CatalogPlugin):
#print "fetchBooksByTitle(): opts.search_text: %s" % self.opts.search_text
# Fetch the database as a dictionary
data = self.plugin.search_sort_db(self.db, self.opts)
+ data = self.processExclusions(data)
# Populate this_title{} from data[{},{}]
titles = []
for record in data:
+ if False:
+ print "available record metadata:\n%s" % sorted(record.keys())
this_title = {}
this_title['id'] = record['id']
@@ -1360,6 +1462,9 @@ class EPUB_MOBI(CatalogPlugin):
this_title['title_sort'] = self.generateSortTitle(this_title['title'])
if 'authors' in record:
+ # from calibre.ebooks.metadata import authors_to_string
+ # return authors_to_string(self.authors)
+
this_title['authors'] = record['authors']
if record['authors']:
this_title['author'] = " & ".join(record['authors'])
@@ -1388,6 +1493,8 @@ class EPUB_MOBI(CatalogPlugin):
record['comments'] = record['comments'][:ad_offset]
this_title['description'] = self.markdownComments(record['comments'])
+
+ # Create short description
paras = BeautifulSoup(this_title['description']).findAll('p')
tokens = []
for p in paras:
@@ -1399,6 +1506,10 @@ class EPUB_MOBI(CatalogPlugin):
this_title['description'] = None
this_title['short_description'] = None
+ # Merge with custom field/value
+ if self.__merge_comments['field']:
+ this_title['description'] = self.mergeComments(this_title)
+
if record['cover']:
this_title['cover'] = re.sub('&', '&', record['cover'])
@@ -1413,6 +1524,21 @@ class EPUB_MOBI(CatalogPlugin):
formats.append(self.convertHTMLEntities(format))
this_title['formats'] = formats
+ # Add user notes to be displayed in header
+ # Special case handling for datetime fields
+ if self.opts.header_note_source_field:
+ field_md = self.__db.metadata_for_field(self.opts.header_note_source_field)
+ notes = self.__db.get_field(record['id'],
+ self.opts.header_note_source_field,
+ index_is_id=True)
+ if notes and field_md['datatype'] == 'datetime':
+ # Reformat date fields to match UI presentation: dd MMM YYYY
+ notes = format_date(notes,'dd MMM yyyy')
+
+ if notes:
+ this_title['notes'] = {'source':field_md['name'],
+ 'content':notes}
+
titles.append(this_title)
# Re-sort based on title_sort
@@ -1610,183 +1736,10 @@ class EPUB_MOBI(CatalogPlugin):
(title_num, len(self.booksByTitle)),
float(title_num*100/len(self.booksByTitle))/100)
- # Generate the header
- soup = self.generateHTMLDescriptionHeader("%s" % title['title'])
- body = soup.find('body')
-
- btc = 0
-
- # Insert the anchor
- aTag = Tag(soup, "a")
- aTag['name'] = "book%d" % int(title['id'])
- body.insert(btc, aTag)
- btc += 1
-
- # Insert the book title
- #
Book Title
- emTag = Tag(soup, "em")
- if title['series']:
- # title
series series_index
- if self.opts.generate_series:
- brTag = Tag(soup,'br')
- title_tokens = list(title['title'].partition(':'))
- emTag.insert(0, escape(NavigableString(title_tokens[2].strip())))
- emTag.insert(1, brTag)
- smallTag = Tag(soup,'small')
- aTag = Tag(soup,'a')
- aTag['href'] = "%s.html#%s_series" % ('BySeries',
- re.sub('\W','',title['series']).lower())
- aTag.insert(0, title_tokens[0])
- smallTag.insert(0, aTag)
- emTag.insert(2, smallTag)
- else:
- brTag = Tag(soup,'br')
- title_tokens = list(title['title'].partition(':'))
- emTag.insert(0, escape(NavigableString(title_tokens[2].strip())))
- emTag.insert(1, brTag)
- smallTag = Tag(soup,'small')
- smallTag.insert(0, escape(NavigableString(title_tokens[0])))
- emTag.insert(2, smallTag)
- else:
- emTag.insert(0, NavigableString(escape(title['title'])))
- titleTag = body.find(attrs={'class':'title'})
- titleTag.insert(0,emTag)
-
- # Create the author anchor
- authorTag = body.find(attrs={'class':'author'})
- aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
- self.generateAuthorAnchor(title['author']))
- aTag.insert(0, title['author'])
-
- # Prefix author with read|reading|none symbol or missing symbol
- if self.opts.wishlist_tag in title.get('tags', []):
- authorTag.insert(0, NavigableString(self.MISSING_SYMBOL + " by "))
- else:
- if title['read']:
- authorTag.insert(0, NavigableString(self.READ_SYMBOL + " by "))
- elif self.opts.connected_kindle and title['id'] in self.bookmarked_books:
- authorTag.insert(0, NavigableString(self.READING_SYMBOL + " by "))
- else:
- #authorTag.insert(0, NavigableString(self.NOT_READ_SYMBOL + " by "))
- authorTag.insert(0, NavigableString("by "))
- authorTag.insert(1, aTag)
-
- '''
- # Insert Series info or remove.
- seriesTag = body.find(attrs={'class':'series'})
- if title['series']:
- # Insert a spacer to match the author indent
- stc = 0
- fontTag = Tag(soup,"font")
- fontTag['style'] = 'color:white;font-size:large'
- if self.opts.fmt == 'epub':
- fontTag['style'] += ';opacity: 0.0'
- fontTag.insert(0, NavigableString("by "))
- seriesTag.insert(stc, fontTag)
- stc += 1
- if float(title['series_index']) - int(title['series_index']):
- series_str = 'Series: %s [%4.2f]' % (title['series'], title['series_index'])
- else:
- series_str = '%s [%d]' % (title['series'], title['series_index'])
- seriesTag.insert(stc,NavigableString(series_str))
- else:
- seriesTag.extract()
- '''
- # Insert linked genres
- if 'tags' in title:
- tagsTag = body.find(attrs={'class':'tags'})
- ttc = 0
-
- '''
- # Insert a spacer to match the author indent
- fontTag = Tag(soup,"font")
- fontTag['style'] = 'color:white;font-size:large'
- if self.opts.fmt == 'epub':
- fontTag['style'] += ';opacity: 0.0'
- fontTag.insert(0, NavigableString(" by "))
- tagsTag.insert(ttc, fontTag)
- ttc += 1
- '''
-
- for tag in title.get('tags', []):
- aTag = Tag(soup,'a')
- #print "aTag: %s" % "Genre_%s.html" % re.sub("\W","",tag.lower())
- aTag['href'] = "Genre_%s.html" % re.sub("\W","",tag.lower())
- aTag.insert(0,escape(NavigableString(tag)))
- emTag = Tag(soup, "em")
- emTag.insert(0, aTag)
- if ttc < len(title['tags'])-1:
- emTag.insert(1, NavigableString(' · '))
- tagsTag.insert(ttc, emTag)
- ttc += 1
-
- # Insert formats
- if 'formats' in title:
- formatsTag = body.find(attrs={'class':'formats'})
- formats = []
- for format in sorted(title['formats']):
- formats.append(format.rpartition('.')[2].upper())
- formatsTag.insert(0, NavigableString(' · '.join(formats)))
-
- # Insert the cover
if available
- imgTag = Tag(soup,"img")
- if 'cover' in title:
- imgTag['src'] = "../images/thumbnail_%d.jpg" % int(title['id'])
- else:
- imgTag['src'] = "../images/thumbnail_default.jpg"
- imgTag['alt'] = "cover"
-
- '''
- if self.opts.fmt == 'mobi':
- imgTag['style'] = 'width: %dpx; height:%dpx;' % (self.thumbWidth, self.thumbHeight)
- '''
-
- thumbnailTag = body.find(attrs={'class':'thumbnail'})
- thumbnailTag.insert(0,imgTag)
-
- # Insert the publisher
- publisherTag = body.find(attrs={'class':'publisher'})
- if 'publisher' in title:
- publisherTag.insert(0,NavigableString(title['publisher'] + '
' ))
- else:
- publisherTag.insert(0,NavigableString('
'))
-
- # Insert the publication date
- pubdateTag = body.find(attrs={'class':'date'})
- if title['date'] is not None:
- pubdateTag.insert(0,NavigableString(title['date'] + '
'))
- else:
- pubdateTag.insert(0,NavigableString('
'))
-
- # Insert the rating, remove if unrated
- # Render different ratings chars for epub/mobi
- stars = int(title['rating']) / 2
- ratingTag = body.find(attrs={'class':'rating'})
- if stars:
- star_string = self.FULL_RATING_SYMBOL * stars
- empty_stars = self.EMPTY_RATING_SYMBOL * (5 - stars)
- ratingTag.insert(0,NavigableString('%s%s
' % (star_string,empty_stars)))
- else:
- #ratingLabel = body.find('td',text="Rating").replaceWith("Unrated")
- ratingTag.insert(0,NavigableString('
'))
+ # Generate the header from user-customizable template
+ soup = self.generateHTMLDescriptionHeader(title)
- # Insert user notes or remove Notes label. Notes > 1 line will push formatting down
- if 'notes' in title:
- notesTag = body.find(attrs={'class':'notes'})
- notesTag.insert(0,NavigableString(title['notes'] + '
'))
- else:
- pass
-# notes_labelTag = body.find(attrs={'class':'notes_label'})
-# empty_labelTag = Tag(soup, "td")
-# empty_labelTag.insert(0,NavigableString('
'))
-# notes_labelTag.replaceWith(empty_labelTag)
-
- # Insert the blurb
- if 'description' in title and title['description'] > '':
- blurbTag = body.find(attrs={'class':'description'})
- blurbTag.insert(0,NavigableString(title['description']))
# Write the book entry to contentdir
outfile = open("%s/book_%d.html" % (self.contentDir, int(title['id'])), 'w')
@@ -1854,9 +1807,17 @@ class EPUB_MOBI(CatalogPlugin):
title_list = self.booksByTitle
if not self.useSeriesPrefixInTitlesSection:
title_list = self.booksByTitle_noSeriesPrefix
+ drtc = 0
+ divRunningTag = None
for book in title_list:
if self.letter_or_symbol(book['title_sort'][0]) != current_letter :
# Start a new letter
+ if drtc and divRunningTag is not None:
+ divTag.insert(dtc, divRunningTag)
+ dtc += 1
+ divRunningTag = Tag(soup, 'div')
+ divRunningTag['style'] = 'display:inline-block;width:100%'
+ drtc = 0
current_letter = self.letter_or_symbol(book['title_sort'][0])
pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index"
@@ -1864,8 +1825,8 @@ class EPUB_MOBI(CatalogPlugin):
aTag['name'] = "%s" % self.letter_or_symbol(book['title_sort'][0])
pIndexTag.insert(0,aTag)
pIndexTag.insert(1,NavigableString(self.letter_or_symbol(book['title_sort'][0])))
- divTag.insert(dtc,pIndexTag)
- dtc += 1
+ divRunningTag.insert(dtc,pIndexTag)
+ drtc += 1
# Add books
pBookTag = Tag(soup, "p")
@@ -1894,7 +1855,8 @@ class EPUB_MOBI(CatalogPlugin):
# Link to book
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
aTag.insert(0,escape(book['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -1912,7 +1874,13 @@ class EPUB_MOBI(CatalogPlugin):
pBookTag.insert(ptc, emTag)
ptc += 1
- divTag.insert(dtc, pBookTag)
+ if divRunningTag is not None:
+ divRunningTag.insert(drtc, pBookTag)
+ drtc += 1
+
+ # Add the last divRunningTag to divTag
+ if divRunningTag is not None:
+ divTag.insert(dtc, divRunningTag)
dtc += 1
# Add the divTag to the body
@@ -1924,7 +1892,7 @@ class EPUB_MOBI(CatalogPlugin):
outfile = open(outfile_spec, 'w')
outfile.write(soup.prettify())
outfile.close()
- self.htmlFileList.append("content/ByAlphaTitle.html")
+ self.htmlFileList_1.append("content/ByAlphaTitle.html")
def generateHTMLByAuthor(self):
# Write books by author A-Z
@@ -1972,31 +1940,41 @@ class EPUB_MOBI(CatalogPlugin):
# Loop through booksByAuthor
book_count = 0
+ divRunningTag = None
for book in self.booksByAuthor:
book_count += 1
if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter :
- '''
- # Start a new letter - anchor only, hidden
- current_letter = book['author_sort'][0].upper()
- aTag = Tag(soup, "a")
- aTag['name'] = "%sauthors" % current_letter
- divTag.insert(dtc, aTag)
- dtc += 1
- '''
# Start a new letter with Index letter
current_letter = self.letter_or_symbol(book['author_sort'][0].upper())
+ author_count = 0
+ divOpeningTag = Tag(soup, 'div')
+ divOpeningTag['style'] = 'display:inline-block;width:100%'
+ dotc = 0
pIndexTag = Tag(soup, "p")
pIndexTag['class'] = "letter_index"
aTag = Tag(soup, "a")
aTag['name'] = "%sauthors" % self.letter_or_symbol(current_letter)
pIndexTag.insert(0,aTag)
pIndexTag.insert(1,NavigableString(self.letter_or_symbol(book['author_sort'][0].upper())))
- divTag.insert(dtc,pIndexTag)
- dtc += 1
+ divOpeningTag.insert(dotc,pIndexTag)
+ dotc += 1
if book['author'] != current_author:
# Start a new author
current_author = book['author']
+ author_count += 1
+ if author_count == 2:
+ # Add divOpeningTag to divTag
+ divTag.insert(dtc, divOpeningTag)
+ dtc += 1
+ elif author_count > 2 and divRunningTag is not None:
+ divTag.insert(dtc, divRunningTag)
+ dtc += 1
+
+ divRunningTag = Tag(soup, 'div')
+ divRunningTag['style'] = 'display:inline-block;width:100%'
+ drtc = 0
+
non_series_books = 0
current_series = None
pAuthorTag = Tag(soup, "p")
@@ -2005,18 +1983,12 @@ class EPUB_MOBI(CatalogPlugin):
aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
aTag.insert(0,NavigableString(current_author))
pAuthorTag.insert(0,aTag)
- divTag.insert(dtc,pAuthorTag)
- dtc += 1
-
- '''
- # Insert an
between non-series and series
- if not current_series and non_series_books and book['series']:
- # Insert an
- hrTag = Tag(soup,'hr')
- hrTag['class'] = "series_divider"
- divTag.insert(dtc,hrTag)
- dtc += 1
- '''
+ if author_count == 1:
+ divOpeningTag.insert(dotc, pAuthorTag)
+ dotc += 1
+ elif divRunningTag is not None:
+ divRunningTag.insert(drtc,pAuthorTag)
+ drtc += 1
# Check for series
if book['series'] and book['series'] != current_series:
@@ -2036,8 +2008,12 @@ class EPUB_MOBI(CatalogPlugin):
#pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series']))
pSeriesTag.insert(0,NavigableString('%s' % book['series']))
- divTag.insert(dtc,pSeriesTag)
- dtc += 1
+ if author_count == 1:
+ divOpeningTag.insert(dotc, pSeriesTag)
+ dotc += 1
+ elif divRunningTag is not None:
+ divRunningTag.insert(drtc,pSeriesTag)
+ drtc += 1
if current_series and not book['series']:
current_series = None
@@ -2067,8 +2043,9 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
- # Use series, series index if avail else just title, + year of publication
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ # Use series, series index if avail else title, + year of publication
if current_series:
aTag.insert(0,'%s (%s)' % (escape(book['title'][len(book['series'])+1:]),
book['date'].split()[1]))
@@ -2079,9 +2056,12 @@ class EPUB_MOBI(CatalogPlugin):
pBookTag.insert(ptc, aTag)
ptc += 1
-
- divTag.insert(dtc, pBookTag)
- dtc += 1
+ if author_count == 1:
+ divOpeningTag.insert(dotc, pBookTag)
+ dotc += 1
+ elif divRunningTag is not None:
+ divRunningTag.insert(drtc,pBookTag)
+ drtc += 1
if not self.__generateForKindle:
# Insert the tag with book_count at the head
@@ -2097,6 +2077,10 @@ class EPUB_MOBI(CatalogPlugin):
body.insert(btc,pTag)
btc += 1
+ if author_count == 1:
+ divTag.insert(dtc, divOpeningTag)
+ dtc += 1
+
# Add the divTag to the body
body.insert(btc, divTag)
@@ -2106,7 +2090,7 @@ class EPUB_MOBI(CatalogPlugin):
outfile = open(outfile_spec, 'w')
outfile.write(soup.prettify())
outfile.close()
- self.htmlFileList.append("content/ByAlphaAuthor.html")
+ self.htmlFileList_1.append("content/ByAlphaAuthor.html")
def generateHTMLByDateAdded(self):
# Write books by reverse chronological order
@@ -2200,7 +2184,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
if current_series:
aTag.insert(0,escape(new_entry['title'][len(new_entry['series'])+1:]))
else:
@@ -2251,7 +2236,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2379,7 +2365,7 @@ class EPUB_MOBI(CatalogPlugin):
outfile = open(outfile_spec, 'w')
outfile.write(soup.prettify())
outfile.close()
- self.htmlFileList.append("content/ByDateAdded.html")
+ self.htmlFileList_2.append("content/ByDateAdded.html")
def generateHTMLByDateRead(self):
# Write books by active bookmarks
@@ -2411,7 +2397,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2458,7 +2445,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
pBookTag.insert(ptc, aTag)
ptc += 1
@@ -2568,7 +2556,7 @@ class EPUB_MOBI(CatalogPlugin):
outfile = open(outfile_spec, 'w')
outfile.write(soup.prettify())
outfile.close()
- self.htmlFileList.append("content/ByDateRead.html")
+ self.htmlFileList_2.append("content/ByDateRead.html")
def generateHTMLBySeries(self):
'''
@@ -2599,7 +2587,9 @@ class EPUB_MOBI(CatalogPlugin):
self.opts.search_text = search_phrase
# Fetch the database as a dictionary
- self.booksBySeries = self.plugin.search_sort_db(self.db, self.opts)
+ data = self.plugin.search_sort_db(self.db, self.opts)
+ self.booksBySeries = self.processExclusions(data)
+
if not self.booksBySeries:
self.opts.generate_series = False
self.opts.log(" no series found in selected books, cancelling series generation")
@@ -2638,14 +2628,6 @@ class EPUB_MOBI(CatalogPlugin):
# Check for initial letter change
sort_title = self.generateSortTitle(book['series'])
if self.letter_or_symbol(sort_title[0].upper()) != current_letter :
- '''
- # Start a new letter - anchor only, hidden
- current_letter = book['author_sort'][0].upper()
- aTag = Tag(soup, "a")
- aTag['name'] = "%sseries" % current_letter
- divTag.insert(dtc, aTag)
- dtc += 1
- '''
# Start a new letter with Index letter
current_letter = self.letter_or_symbol(sort_title[0].upper())
pIndexTag = Tag(soup, "p")
@@ -2699,7 +2681,8 @@ class EPUB_MOBI(CatalogPlugin):
ptc += 1
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else just title
#aTag.insert(0,'%d. %s · %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors'])))
@@ -2747,7 +2730,7 @@ class EPUB_MOBI(CatalogPlugin):
outfile = open(outfile_spec, 'w')
outfile.write(soup.prettify())
outfile.close()
- self.htmlFileList.append("content/BySeries.html")
+ self.htmlFileList_1.append("content/BySeries.html")
def generateHTMLByTags(self):
# Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ...
@@ -2922,6 +2905,11 @@ class EPUB_MOBI(CatalogPlugin):
title['cover'] = cover
self.generateThumbnail(title, image_dir, "thumbnail_default.jpg")
+ # Write the thumb_width to the file validating cache contents
+ # Allows detection of aborted catalog builds
+ with closing(ZipFile(self.__archive_path, mode='a'))as zfw:
+ zfw.writestr('thumb_width', self.opts.thumb_width)
+
self.thumbs = thumbs
def generateOPF(self):
@@ -2983,22 +2971,25 @@ class EPUB_MOBI(CatalogPlugin):
manifest.insert(mtc, itemTag)
mtc += 1
- # Write the thumbnail images to the manifest
- for thumb in self.thumbs:
- itemTag = Tag(soup, "item")
- itemTag['href'] = "images/%s" % (thumb)
- end = thumb.find('.jpg')
- itemTag['id'] = "%s-image" % thumb[:end]
- itemTag['media-type'] = 'image/jpeg'
- manifest.insert(mtc, itemTag)
- mtc += 1
+ # Write the thumbnail images, descriptions to the manifest
+ sort_descriptions_by = []
+ if self.opts.generate_descriptions:
+ for thumb in self.thumbs:
+ itemTag = Tag(soup, "item")
+ itemTag['href'] = "images/%s" % (thumb)
+ end = thumb.find('.jpg')
+ itemTag['id'] = "%s-image" % thumb[:end]
+ itemTag['media-type'] = 'image/jpeg'
+ manifest.insert(mtc, itemTag)
+ mtc += 1
- # HTML files - add books to manifest and spine
- sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
+ # HTML files - add descriptions to manifest and spine
+ sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
else self.booksByTitle
# Add html_files to manifest and spine
- for file in self.htmlFileList:
+ for file in self.htmlFileList_1:
+ # By Author, By Title, By Series,
itemTag = Tag(soup, "item")
start = file.find('/') + 1
end = file.find('.')
@@ -3032,6 +3023,23 @@ class EPUB_MOBI(CatalogPlugin):
spine.insert(stc, itemrefTag)
stc += 1
+ for file in self.htmlFileList_2:
+ # By Date Added, By Date Read
+ itemTag = Tag(soup, "item")
+ start = file.find('/') + 1
+ end = file.find('.')
+ itemTag['href'] = file
+ itemTag['id'] = file[start:end].lower()
+ itemTag['media-type'] = "application/xhtml+xml"
+ manifest.insert(mtc, itemTag)
+ mtc += 1
+
+ # spine
+ itemrefTag = Tag(soup, "itemref")
+ itemrefTag['idref'] = file[start:end].lower()
+ spine.insert(stc, itemrefTag)
+ stc += 1
+
for book in sort_descriptions_by:
# manifest
itemTag = Tag(soup, "item")
@@ -3970,15 +3978,15 @@ class EPUB_MOBI(CatalogPlugin):
from calibre.customize.ui import output_profiles
for x in output_profiles():
if x.short_name == self.opts.output_profile:
- # .9" width aspect ratio: 3:4
- self.thumbWidth = int(x.dpi * 1)
- self.thumbHeight = int(self.thumbWidth * 1.33)
+ # aspect ratio: 3:4
+ self.thumbWidth = x.dpi * float(self.opts.thumb_width)
+ self.thumbHeight = self.thumbWidth * 1.33
if 'kindle' in x.short_name and self.opts.fmt == 'mobi':
# Kindle DPI appears to be off by a factor of 2
- self.thumbWidth = int(self.thumbWidth/2)
- self.thumbHeight = int(self.thumbHeight/2)
+ self.thumbWidth = self.thumbWidth/2
+ self.thumbHeight = self.thumbHeight/2
break
- if False and self.verbose:
+ if True and self.verbose:
self.opts.log(" DPI = %d; thumbnail dimensions: %d x %d" % \
(x.dpi, self.thumbWidth, self.thumbHeight))
@@ -4043,9 +4051,13 @@ class EPUB_MOBI(CatalogPlugin):
field,
index_is_id=True)
if field_contents:
- if re.search(pat, unicode(field_contents),
- re.IGNORECASE) is not None:
- return True
+ try:
+ if re.search(pat, unicode(field_contents),
+ re.IGNORECASE) is not None:
+ return True
+ except:
+ # Compiling of pat failed, ignore it
+ pass
return False
@@ -4238,7 +4250,8 @@ class EPUB_MOBI(CatalogPlugin):
# Add the book title
aTag = Tag(soup, "a")
- aTag['href'] = "book_%d.html" % (int(float(book['id'])))
+ if self.opts.generate_descriptions:
+ aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else just title
if current_series:
aTag.insert(0,escape(book['title'][len(book['series'])+1:]))
@@ -4263,59 +4276,196 @@ class EPUB_MOBI(CatalogPlugin):
return titles_spanned
- def generateHTMLDescriptionHeader(self, title):
+ def generateHTMLDescriptionHeader(self, book):
+ '''
+ Generate description header from template
+ '''
+ NBSP = ' '
+ MIDDOT = '·'
+ def generate_html():
+ args = dict(
+ author=author,
+ author_prefix=author_prefix,
+ css=css,
+ formats=formats,
+ genres=genres,
+ note_content=note_content,
+ note_source=note_source,
+ pubdate=pubdate,
+ publisher=publisher,
+ pubmonth=pubmonth,
+ pubyear=pubyear,
+ rating=rating,
+ series=series,
+ series_index=series_index,
+ title=title,
+ title_str=title_str,
+ xmlns=XHTML_NS,
+ )
- title_border = '' if self.opts.fmt == 'epub' else \
- '
'
- header = '''
-
-
-
-
-
-
-
-
-
- {0}
-
-
-
-
-
-
- |
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
-
-
- '''.format(title_border)
+ generated_html = P('catalog/template.xhtml',
+ data=True).decode('utf-8').format(**args)
- # Insert the supplied title
+ soup = BeautifulSoup(generated_html)
+ return soup.renderContents(None)
+
+ if False:
+ print "title metadata:\n%s" % ', '.join(sorted(book.keys()))
+ if False:
+ for item in sorted(book.keys()):
+ try:
+ print "%s: %s%s" % (item, book[item][:50], '...' if len(book[item])>50 else '')
+ except:
+ print "%s: %s" % (item, book[item])
+
+ # Generate the template arguments
+ css = P('catalog/stylesheet.css', data=True).decode('utf-8')
+ title_str = escape(book['title'])
+
+ # Title/series
+ if book['series']:
+ series_id, _, title = book['title'].partition(':')
+ title = escape(title.strip())
+ series = escape(book['series'])
+ series_index = str(book['series_index'])
+ if series_index.endswith('.0'):
+ series_index = series_index[:-2]
+ else:
+ title = escape(book['title'])
+ series = ''
+ series_index = ''
+
+ # 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 "
+ elif self.opts.connected_kindle and book['id'] in self.bookmarked_books:
+ author_prefix = self.READING_SYMBOL + " by "
+ else:
+ author_prefix = "by "
+
+ # Genres
+ genres = ''
+ if 'tags' in book:
+ _soup = BeautifulSoup('')
+ genresTag = Tag(_soup,'p')
+ gtc = 0
+ for (i, tag) in enumerate(book.get('tags', [])):
+ aTag = Tag(_soup,'a')
+ if self.opts.generate_genres:
+ aTag['href'] = "Genre_%s.html" % re.sub("\W","",tag.lower())
+ aTag.insert(0,escape(NavigableString(tag)))
+ genresTag.insert(gtc, aTag)
+ gtc += 1
+ if i < len(book['tags'])-1:
+ genresTag.insert(gtc, NavigableString(' %s ' % MIDDOT))
+ gtc += 1
+ genres = genresTag.renderContents()
+
+ # Formats
+ formats = []
+ if 'formats' in book:
+ for format in sorted(book['formats']):
+ formats.append(format.rpartition('.')[2].upper())
+ formats = ' %s ' % MIDDOT.join(formats)
+
+ pubdate = book['date']
+ pubmonth, pubyear = pubdate.split(' ')
+
+ '''
+ # Thumb
+ # This doesn't make it through the etree.fromstring parsing
+ _soup = BeautifulSoup('')
+ imgTag = Tag(_soup,"img")
+ if 'cover' in book:
+ imgTag['src'] = "../images/thumbnail_%d.jpg" % int(book['id'])
+ else:
+ imgTag['src'] = "../images/thumbnail_default.jpg"
+ imgTag['alt'] = "cover thumbnail"
+ thumb = imgTag.renderContents()
+ '''
+
+ # Publisher
+ publisher = NBSP
+ if 'publisher' in book:
+ publisher = book['publisher']
+
+ # Rating
+ stars = int(book['rating']) / 2
+ rating = NBSP
+ if stars:
+ star_string = self.FULL_RATING_SYMBOL * stars
+ empty_stars = self.EMPTY_RATING_SYMBOL * (5 - stars)
+ rating = '%s%s
' % (star_string,empty_stars)
+
+ # Notes
+ note_source = NBSP
+ note_content = NBSP
+ if 'notes' in book:
+ note_source = book['notes']['source']
+ note_content = book['notes']['content']
+
+ # >>>> Populate the template <<<<
+ root = etree.fromstring(generate_html(), parser=RECOVER_PARSER)
+ header = etree.tostring(root, pretty_print=True, encoding='utf-8')
soup = BeautifulSoup(header, selfClosingTags=['mbp:pagebreak'])
- titleTag = soup.find('title')
- titleTag.insert(0,NavigableString(escape(title)))
+
+
+ # >>>> Post-process the template <<<<
+ body = soup.find('body')
+ btc = 0
+ # Insert the title anchor for inbound links
+ aTag = Tag(soup, "a")
+ aTag['name'] = "book%d" % int(book['id'])
+ body.insert(btc, aTag)
+ btc += 1
+
+ # Insert the link to the series or remove
+ aTag = body.find('a', attrs={'class':'series_id'})
+ if book['series']:
+ if self.opts.generate_series:
+ aTag['href'] = "%s.html#%s_series" % ('BySeries',
+ re.sub('\W','',book['series']).lower())
+ else:
+ aTag.extract()
+
+ # Insert the author link (always)
+ aTag = body.find('a', attrs={'class':'author'})
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
+ self.generateAuthorAnchor(book['author']))
+
+ if not genres:
+ genresTag = body.find('p',attrs={'class':'genres'})
+ genresTag.extract()
+
+ if not formats:
+ formatsTag = body.find('p',attrs={'class':'formats'})
+ formatsTag.extract()
+
+ if note_content == NBSP:
+ tdTag = body.find('td', attrs={'class':'notes'})
+ tdTag.contents[0].replaceWith(NBSP)
+
+ # Cover thumb
+ tdTag = body.find('td', attrs={'class':'thumbnail'})
+ imgTag = Tag(soup,"img")
+ if 'cover' in book:
+ imgTag['src'] = "../images/thumbnail_%d.jpg" % int(book['id'])
+ else:
+ imgTag['src'] = "../images/thumbnail_default.jpg"
+ imgTag['alt'] = "cover thumbnail"
+ tdTag.insert(0,imgTag)
+
+ # The Blurb
+ if 'description' in book and book['description'] > '':
+ blurbTag = body.find(attrs={'class':'description'})
+ blurbTag.insert(0,NavigableString(book['description']))
+
+ if False:
+ print soup.prettify()
return soup
def generateHTMLEmptyHeader(self, title):
@@ -4348,7 +4498,6 @@ class EPUB_MOBI(CatalogPlugin):
-