From 3e0c43283d54a36c2574801070171cee33d974af Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Apr 2011 21:43:10 +0100 Subject: [PATCH 01/32] Change author_sort_copy_method default from invert to comma. --- resources/default_tweaks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index c4c951f980..091aa9a34d 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -48,7 +48,7 @@ authors_completer_append_separator = False # When this tweak is changed, the author_sort values stored with each author # must be recomputed by right-clicking on an author in the left-hand tags pane, # selecting 'manage authors', and pressing 'Recalculate all author sort values'. -author_sort_copy_method = 'invert' +author_sort_copy_method = 'comma' #: Use author sort in Tag Browser # Set which author field to display in the tags pane (the list of authors, From 5312b49d85132787bc4f18d91fa7a8230561e298 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 08:59:53 +0100 Subject: [PATCH 02/32] Divide ratings by 2 when used in a template. --- src/calibre/ebooks/metadata/book/base.py | 2 ++ src/calibre/library/save_to_disk.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 167ae52fa3..57c267319f 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -625,6 +625,8 @@ class Metadata(object): res = res + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) + elif datatype == 'rating': + res = res/2 return (name, unicode(res), orig_res, fmeta) return (None, None, None, None) diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 3c57af40a8..42e6c8b156 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -198,7 +198,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250, for key in custom_metadata: if key in format_args: cm = custom_metadata[key] - ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't... if cm['datatype'] == 'series': format_args[key] = title_sort(format_args[key], order=tsorder) if key+'_index' in format_args: From 1e1b530113636d344c59e2c93fdabc3ca974a5cf Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 09:46:43 +0100 Subject: [PATCH 03/32] Add the ability to get the calibre field 'size' using a formatter function booksize() --- src/calibre/library/database2.py | 1 + src/calibre/manual/template_lang.rst | 1 + src/calibre/utils/formatter_functions.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bdcefd13a2..d585c60aef 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -853,6 +853,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.pubdate = row[fm['pubdate']] mi.uuid = row[fm['uuid']] mi.title_sort = row[fm['sort']] + mi.book_size = row[fm['size']] mi.last_modified = row[fm['last_modified']] formats = row[fm['formats']] if not formats: diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index cdb8df2e2b..a77f0d1697 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -230,6 +230,7 @@ The following functions are available in addition to those described in single-f * ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression + * ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7957bd0749..aa8e4fb3a3 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -549,8 +549,22 @@ class BuiltinCapitalize(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) +class BuiltinBooksize(BuiltinFormatterFunction): + name = 'booksize' + arg_count = 0 + doc = _('booksize() -- return value of the field capitalized') + + def evaluate(self, formatter, kwargs, mi, locals): + if mi.book_size is not None: + try: + return str(mi.book_size) + except: + pass + return '' + builtin_add = BuiltinAdd() builtin_assign = BuiltinAssign() +builtin_booksize = BuiltinBooksize() builtin_capitalize = BuiltinCapitalize() builtin_cmp = BuiltinCmp() builtin_contains = BuiltinContains() From d95d6eca67f7fc6f6b06cf2ab8d70477ffec9048 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 10:07:10 +0100 Subject: [PATCH 04/32] Improve performance of get_categories. --- src/calibre/library/database2.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d585c60aef..bc0a8235e4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1379,13 +1379,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for (cat, dex, mult, is_comp) in md: if not book[dex]: continue + tid_cat = tids[cat] + tcats_cat = tcategories[cat] if not mult: val = book[dex] if is_comp: - item = tcategories[cat].get(val, None) + item = tcats_cat.get(val, None) if not item: item = tag_class(val, val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id = val if rating > 0: @@ -1393,11 +1395,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): item.rc += 1 continue try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id @@ -1411,21 +1413,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if is_comp: vals = [v.strip() for v in vals if v.strip()] for val in vals: - if val not in tids: - tids[cat][val] = (val, val) - item = tcategories[cat].get(val, None) - if not item: - item = tag_class(val, val) - tcategories[cat][val] = item - item.c += 1 - item.id = val + if val not in tid_cat: + tid_cat[val] = (val, val) for val in vals: try: - (item_id, sort_val) = tids[cat][val] # let exceptions fly - item = tcategories[cat].get(val, None) + (item_id, sort_val) = tid_cat[val] # let exceptions fly + item = tcats_cat.get(val, None) if not item: item = tag_class(val, sort_val) - tcategories[cat][val] = item + tcats_cat[val] = item item.c += 1 item.id_set.add(book[0]) item.id = item_id From b4dda2792357736ec155412f46facce563afb274 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 13:42:09 +0100 Subject: [PATCH 05/32] Very strange change to prevent calibre from dieing when device_metadata_available or device_connection_changed is emitted. --- src/calibre/gui2/device.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 99cb5848ba..d00aa031f5 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -7,7 +7,7 @@ import os, traceback, Queue, time, cStringIO, re, sys from threading import Thread from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \ - Qt, pyqtSignal, QDialog + Qt, pyqtSignal, QDialog, QObject from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins @@ -587,8 +587,7 @@ class DeviceMenu(QMenu): # {{{ # }}} -class DeviceMixin(object): # {{{ - +class DeviceSignals(QObject): #: This signal is emitted once, after metadata is downloaded from the #: connected device. #: The sequence: gui.device_manager.is_device_connected will become True, @@ -599,6 +598,10 @@ class DeviceMixin(object): # {{{ device_metadata_available = pyqtSignal() device_connection_changed = pyqtSignal(object) +device_signals = DeviceSignals() + +class DeviceMixin(object): # {{{ + def __init__(self): self.device_error_dialog = error_dialog(self, _('Error'), _('Error communicating with device'), ' ') @@ -745,7 +748,7 @@ class DeviceMixin(object): # {{{ self.location_manager.update_devices() self.library_view.set_device_connected(self.device_connected) self.refresh_ondevice() - self.device_connection_changed.emit(connected) + device_signals.device_connection_changed.emit(connected) def info_read(self, job): ''' @@ -784,7 +787,7 @@ class DeviceMixin(object): # {{{ self.sync_news() self.sync_catalogs() self.refresh_ondevice() - self.device_metadata_available.emit() + device_signals.device_metadata_available.emit() def refresh_ondevice(self, reset_only = False): ''' From d7f17e9a1975af262e2c894bfa0815cd04fdb9c1 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 14:28:59 +0100 Subject: [PATCH 06/32] Improvements to using the current search as a restriction --- src/calibre/gui2/layout.py | 2 -- src/calibre/gui2/search_restriction_mixin.py | 33 ++++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index c72b074463..7250103615 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -156,8 +156,6 @@ class SearchBar(QWidget): # {{{ x = ComboBoxWithHelp(self) x.setMaximumSize(QSize(150, 16777215)) x.setObjectName("search_restriction") - x.setToolTip(_('Books display will be restricted to those matching the ' - 'selected saved search')) l.addWidget(x) parent.search_restriction = x diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 8ef02b34b0..ffebc9e131 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -17,6 +17,10 @@ class SearchRestrictionMixin(object): self.search_restriction.setMinimumContentsLength(10) self.search_restriction.setStatusTip(self.search_restriction.toolTip()) self.search_count.setText(_("(all books)")) + self.search_restriction_tooltip = \ + _('Books display will be restricted to those matching a ' + 'selected saved search') + self.search_restriction.setToolTip(self.search_restriction_tooltip) def apply_named_search_restriction(self, name): if not name: @@ -30,29 +34,38 @@ class SearchRestrictionMixin(object): self.apply_search_restriction(r) def apply_text_search_restriction(self, search): + search = unicode(search) if not search: - self.search_restriction.setItemText(1, _('*Current search')) self.search_restriction.setCurrentIndex(0) else: - self.search_restriction.setCurrentIndex(1) - self.search_restriction.setItemText(1, search) + s = '*' + search + if self.search_restriction.count() > 1: + txt = unicode(self.search_restriction.itemText(2)) + if txt.startswith('*'): + self.search_restriction.setItemText(2, s) + else: + self.search_restriction.insertItem(2, s) + else: + self.search_restriction.insertItem(2, s) + self.search_restriction.setCurrentIndex(2) + self.search_restriction.setToolTip('
' + + self.search_restriction_tooltip + + _(' or the search ') + "'" + search + "'
") self._apply_search_restriction(search) def apply_search_restriction(self, i): - self.search_restriction.setItemText(1, _('*Current search')) if i == 1: - restriction = unicode(self.search.currentText()) - if not restriction: - self.search_restriction.setCurrentIndex(0) - else: - self.search_restriction.setItemText(1, restriction) + self.apply_text_search_restriction(unicode(self.search.currentText())) + elif i == 2 and unicode(self.search_restriction.currentText()).startswith('*'): + self.apply_text_search_restriction( + unicode(self.search_restriction.currentText())[1:]) else: r = unicode(self.search_restriction.currentText()) if r is not None and r != '': restriction = 'search:"%s"'%(r) else: restriction = '' - self._apply_search_restriction(restriction) + self._apply_search_restriction(restriction) def _apply_search_restriction(self, restriction): self.saved_search.clear() From 640c4ff7848ffc7944b970f480dc493a4f4e1060 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 19 Apr 2011 15:02:37 +0100 Subject: [PATCH 07/32] Fix non-escaped '|' when searching for commas in authors using REGEXP_MATCH --- src/calibre/library/caches.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 9523795f28..92c5ca9b3c 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -707,7 +707,10 @@ class ResultCache(SearchQueryParser): # {{{ for loc in location: # location is now an array of field indices if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query - q = query.replace(',', '|'); + if matchkind == REGEXP_MATCH: + q = query.replace(',', r'\|'); + else: + q = query.replace(',', '|'); else: q = query From ccaa7143b5e23785c683f568518d3b2942c270bc Mon Sep 17 00:00:00 2001 From: Kovid Goyal\s*
\s*){3,}', '\s*
', '', text) @@ -100,6 +97,7 @@ class FB2MLizer(object): return text def fb2_header(self): + from calibre.ebooks.oeb.base import OPF metadata = {} metadata['title'] = self.oeb_book.metadata.title[0].value metadata['appname'] = __appname__ @@ -180,6 +178,8 @@ class FB2MLizer(object): return u'' def get_cover(self): + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + cover_href = None # Get the raster cover if it's available. @@ -213,6 +213,8 @@ class FB2MLizer(object): return u'' def get_text(self): + from calibre.ebooks.oeb.base import XHTML + from calibre.ebooks.oeb.stylizer import Stylizer text = [''] # Create main section if there are no others to create @@ -248,6 +250,8 @@ class FB2MLizer(object): ''' This function uses the self.image_hrefs dictionary mapping. It is populated by the dump_text function. ''' + from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES + images = [] for item in self.oeb_book.manifest: # Don't write the image if it's not referenced in the document's text. @@ -344,6 +348,8 @@ class FB2MLizer(object): @return: List of string representing the XHTML converted to FB2 markup. ''' + from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace + # Ensure what we are converting is not a string and that the fist tag is part of the XHTML namespace. if not isinstance(elem_tree.tag, basestring) or namespace(elem_tree.tag) != XHTML_NS: return [] diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index dd0a247a67..079e990de3 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -315,7 +315,8 @@ class HTMLInput(InputFormatPlugin): from calibre import guess_type from calibre.ebooks.oeb.transforms.metadata import \ meta_info_to_oeb_metadata - import cssutils + import cssutils, logging + cssutils.log.setLevel(logging.WARN) self.OEB_STYLES = OEB_STYLES oeb = create_oebbook(log, None, opts, self, encoding=opts.input_encoding, populate=False) diff --git a/src/calibre/ebooks/html/meta.py b/src/calibre/ebooks/html/meta.py index 9a088efb16..07cf9236fc 100644 --- a/src/calibre/ebooks/html/meta.py +++ b/src/calibre/ebooks/html/meta.py @@ -4,7 +4,6 @@ __copyright__ = '2010, Fabian Grassl◆
', re.DOTALL|re.IGNORECASE), lambda match: ''+_('Could not download metadata and/or covers for %d of the books. Click' - ' "Show details" to see which books.')%len(failed_ids) - det_msg = [] - for i in failed_ids | failed_covers: - title = title_map[i] - if i in failed_ids: - title += (' ' + _('(Failed metadata)')) - if i in failed_covers: - title += (' ' + _('(Failed cover)')) - det_msg.append(title) - msg = '
' + _('Finished downloading metadata for %d book(s). ' - 'Proceed with updating the metadata in your library?')%len(id_map) - q = MessageBox(MessageBox.QUESTION, _('Download complete'), - msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids), - parent=gui) + id_map, failed_ids, failed_covers, title_map, all_failed = job.result + if all_failed: + q = error_dialog(gui, _('Download failed'), + _('Failed to download metadata or covers for any of the %d' + ' book(s).') % len(id_map)) + else: + fmsg = det_msg = '' + if failed_ids or failed_covers: + fmsg = '
'+_('Could not download metadata and/or covers for %d of the books. Click' + ' "Show details" to see which books.')%len(failed_ids) + det_msg = [] + for i in failed_ids | failed_covers: + title = title_map[i] + if i in failed_ids: + title += (' ' + _('(Failed metadata)')) + if i in failed_covers: + title += (' ' + _('(Failed cover)')) + det_msg.append(title) + msg = '
' + _('Finished downloading metadata for %d book(s). '
+ 'Proceed with updating the metadata in your library?')%len(id_map)
+ q = MessageBox(MessageBox.QUESTION, _('Download complete'),
+ msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids),
+ parent=gui)
+ q.finished.connect(partial(apply_metadata, job, gui, q))
+
q.vlb = q.bb.addButton(_('View log'), q.bb.ActionRole)
q.vlb.setIcon(QIcon(I('debug.png')))
q.vlb.clicked.connect(partial(view_log, job, q))
q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers))
q.setModal(False)
+ if all_failed:
+ q.det_msg_toggle.setVisible(False)
q.show()
- q.finished.connect(partial(apply_metadata, job, gui, q))
# }}}
@@ -336,6 +344,7 @@ def download(ids, db, do_identify, covers,
title_map = {}
ans = {}
count = 0
+ all_failed = True
for i, mi in izip(ids, metadata):
if abort.is_set():
log.error('Aborting...')
@@ -350,6 +359,7 @@ def download(ids, db, do_identify, covers,
except:
pass
if results:
+ all_failed = False
mi = merge_result(mi, results[0])
identifiers = mi.identifiers
if not mi.is_null('rating'):
@@ -367,6 +377,7 @@ def download(ids, db, do_identify, covers,
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
f.write(cdata[-1])
mi.cover = f.name
+ all_failed = False
else:
failed_covers.add(i)
ans[i] = mi
@@ -374,7 +385,7 @@ def download(ids, db, do_identify, covers,
notifications.put((count/len(ids),
_('Downloaded %d of %d')%(count, len(ids))))
log('Download complete, with %d failures'%len(failed_ids))
- return (ans, failed_ids, failed_covers, title_map)
+ return (ans, failed_ids, failed_covers, title_map, all_failed)
From ac8df2f09962c58f2d7ed8c8696967203c132efb Mon Sep 17 00:00:00 2001
From: Kovid Goyal '+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%len(failed_ids)
- det_msg = []
- for i in failed_ids | failed_covers:
- title = title_map[i]
- if i in failed_ids:
- title += (' ' + _('(Failed metadata)'))
- if i in failed_covers:
- title += (' ' + _('(Failed cover)'))
- det_msg.append(title)
msg = ' ' + _('Finished downloading metadata for %d book(s). '
'Proceed with updating the metadata in your library?')%len(id_map)
q = MessageBox(MessageBox.QUESTION, _('Download complete'),
- msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids),
+ msg + fmsg, det_msg=det_msg, show_copy_button=bool(failed_ids),
parent=gui)
q.finished.connect(partial(apply_metadata, job, gui, q))
@@ -309,8 +311,6 @@ def proceed(gui, job):
q.vlb.clicked.connect(partial(view_log, job, q))
q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers))
q.setModal(False)
- if all_failed:
- q.det_msg_toggle.setVisible(False)
q.show()
# }}}