From ff73865d9e75d5d4e44eb584961209654d7239e9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 21 Sep 2010 14:13:03 +0100 Subject: [PATCH 1/4] Prevent cross-thread lock errors by having the cover cache get the image on the GUI thread. --- src/calibre/gui2/__init__.py | 30 +++++++++++++++++++++++++++++- src/calibre/gui2/library/models.py | 4 ++-- src/calibre/library/caches.py | 7 +++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index e58dce5559..ba32c09e06 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1,7 +1,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' """ The GUI """ -import os, sys +import os, sys, Queue from threading import RLock from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ @@ -296,6 +296,34 @@ class Dispatcher(QObject): def dispatch(self, args, kwargs): self.func(*args, **kwargs) +class FunctionDispatcher(QObject): + ''' + Convenience class to use Qt signals with arbitrary python functions. + By default, ensures that a function call always happens in the + thread this Dispatcher was created in. + ''' + dispatch_signal = pyqtSignal(object, object, object) + + def __init__(self, func, queued=True, parent=None): + QObject.__init__(self, parent) + self.func = func + typ = Qt.QueuedConnection + if not queued: + typ = Qt.AutoConnection if queued is None else Qt.DirectConnection + self.dispatch_signal.connect(self.dispatch, type=typ) + + def __call__(self, *args, **kwargs): + q = Queue.Queue() + self.dispatch_signal.emit(q, args, kwargs) + return q.get() + + def dispatch(self, q, args, kwargs): + try: + res = self.func(*args, **kwargs) + except: + res = None + q.put(res) + class GetMetadata(QObject): ''' Convenience class to ensure that metadata readers are used only in the diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 6941869e44..4b1e974b12 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -12,7 +12,7 @@ from operator import attrgetter from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ QModelIndex, QVariant, QDate -from calibre.gui2 import NONE, config, UNDEFINED_QDATE +from calibre.gui2 import NONE, config, UNDEFINED_QDATE, FunctionDispatcher from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ptempfile import PersistentTemporaryFile @@ -151,7 +151,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.database_changed.emit(db) if self.cover_cache is not None: self.cover_cache.stop() - self.cover_cache = CoverCache(db) + self.cover_cache = CoverCache(db, FunctionDispatcher(self.db.cover)) self.cover_cache.start() def refresh_cover(event, ids): if event == 'cover' and self.cover_cache is not None: diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 5f7fbdccc9..573c1f5797 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -23,10 +23,11 @@ from calibre import fit_image class CoverCache(Thread): - def __init__(self, db): + def __init__(self, db, cover_func): Thread.__init__(self) self.daemon = True self.db = db + self.cover_func = cover_func self.load_queue = Queue() self.keep_running = True self.cache = {} @@ -37,7 +38,9 @@ class CoverCache(Thread): self.keep_running = False def _image_for_id(self, id_): - img = self.db.cover(id_, index_is_id=True, as_image=True) + import time + time.sleep(0.050) # Limit 20/second to not overwhelm the GUI + img = self.cover_func(id_, index_is_id=True, as_image=True) if img is None: img = QImage() if not img.isNull(): From be2210f928d0023ae1f752aeea3fb51f84132e8d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 21 Sep 2010 15:26:02 +0100 Subject: [PATCH 2/4] Add 'start series renumbering from N' to bulk edit. --- src/calibre/gui2/dialogs/metadata_bulk.py | 25 +++++++- src/calibre/gui2/dialogs/metadata_bulk.ui | 69 +++++++++++++++++++---- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 7122fe14fa..8a692d94d5 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -31,7 +31,8 @@ class Worker(Thread): def doit(self): remove, add, au, aus, do_aus, rating, pub, do_series, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ - do_remove_conv, do_auto_author, series = self.args + do_remove_conv, do_auto_author, series, do_series_restart, \ + series_start_value = self.args # first loop: do author and title. These will commit at the end of each # operation, because each operation modifies the file system. We want to @@ -69,7 +70,11 @@ class Worker(Thread): self.db.set_publisher(id, pub, notify=False, commit=False) if do_series: - next = self.db.get_next_series_num_for(series) + if do_series_restart: + next = series_start_value + series_start_value += 1 + else: + next = self.db.get_next_series_num_for(series) self.db.set_series(id, series, notify=False, commit=False) num = next if do_autonumber and series else 1.0 self.db.set_series_index(id, num, notify=False, commit=False) @@ -163,6 +168,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.series.currentIndexChanged[int].connect(self.series_changed) self.series.editTextChanged.connect(self.series_changed) self.tag_editor_button.clicked.connect(self.tag_editor) + self.autonumber_series.stateChanged[int].connect(self.auto_number_changed) if len(db.custom_column_label_map) == 0: self.central_widget.removeTab(1) @@ -538,6 +544,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.tags.update_tags_cache(self.db.all_tags()) self.remove_tags.update_tags_cache(self.db.all_tags()) + def auto_number_changed(self, state): + if state: + self.series_numbering_restarts.setEnabled(True) + self.series_start_number.setEnabled(True) + else: + self.series_numbering_restarts.setEnabled(False) + self.series_numbering_restarts.setChecked(False) + self.series_start_number.setEnabled(False) + self.series_start_number.setValue(1) + def accept(self): if len(self.ids) < 1: return QDialog.accept(self) @@ -566,6 +582,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): do_series = self.write_series series = unicode(self.series.currentText()).strip() do_autonumber = self.autonumber_series.isChecked() + do_series_restart = self.series_numbering_restarts.isChecked() + series_start_value = self.series_start_number.value() do_remove_format = self.remove_format.currentIndex() > -1 remove_format = unicode(self.remove_format.currentText()) do_swap_ta = self.swap_title_and_author.isChecked() @@ -574,7 +592,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): args = (remove, add, au, aus, do_aus, rating, pub, do_series, do_autonumber, do_remove_format, remove_format, do_swap_ta, - do_remove_conv, do_auto_author, series) + do_remove_conv, do_auto_author, series, do_series_restart, + series_start_value) bb = BlockingBusy(_('Applying changes to %d books. This may take a while.') %len(self.ids), parent=self) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index f28f3fb57c..10e22c5df9 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -270,18 +270,63 @@ - - - - Selected books will be automatically numbered, -in the order you selected them. -So if you selected Book A and then Book B, + + + + + + If not checked, the series number for the books will be set to 1. +If checked, selected books will be automatically numbered, in the order +you selected them. So if you selected Book A and then Book B, Book A will have series number 1 and Book B series number 2. - - - Automatically number books in this series - - + + + Automatically number books in this series + + + + + + + false + + + Series will normally be renumbered from the highest number in the database +for that series. Checking this box will tell calibre to start numbering +from the value in the box + + + Force numbers to start with + + + + + + + false + + + 1 + + + 1 + + + + + + + Qt::Horizontal + + + + 20 + 10 + + + + + @@ -599,7 +644,7 @@ nothing should be put between the original text and the inserted text 20 - 40 + 0 From 8a3aa64776aaf11c9909a6518c4f0f90f55a2946 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 21 Sep 2010 15:53:54 +0100 Subject: [PATCH 3/4] Changed sort to use field_metadata.search_term_to_field_key. In the process, refactored field_metadata and LibraryDatabase2 to use the same method names for the same function (in more cases). --- src/calibre/library/caches.py | 11 ++++------- src/calibre/library/database2.py | 4 ++-- src/calibre/library/field_metadata.py | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 573c1f5797..d310a0e6fe 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -334,7 +334,7 @@ class ResultCache(SearchQueryParser): if query and query.strip(): # get metadata key associated with the search term. Eliminates # dealing with plurals and other aliases - location = self.field_metadata.search_term_to_key(location.lower().strip()) + location = self.field_metadata.search_term_to_field_key(location.lower().strip()) if isinstance(location, list): if allow_recursion: for loc in location: @@ -610,12 +610,9 @@ class ResultCache(SearchQueryParser): # Sorting functions {{{ def sanitize_sort_field_name(self, field): - field = field.lower().strip() - if field not in self.field_metadata.iterkeys(): - if field in ('author', 'tag', 'comment'): - field += 's' - if field == 'date': field = 'timestamp' - elif field == 'title': field = 'sort' + field = self.field_metadata.search_term_to_field_key(field.lower().strip()) + # translate some fields to their hidden equivalent + if field == 'title': field = 'sort' elif field == 'authors': field = 'author_sort' return field diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c1ada94a84..77e3afc8a3 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -552,10 +552,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return self.field_metadata.sortable_field_keys() def searchable_fields(self): - return self.field_metadata.searchable_field_keys() + return self.field_metadata.searchable_fields() def search_term_to_field_key(self, term): - return self.field_metadata.search_term_to_key(term) + return self.field_metadata.search_term_to_field_key(term) def metadata_for_field(self, key): return self.field_metadata[key] diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index a8031e5172..bac423f46d 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -501,12 +501,12 @@ class FieldMetadata(dict): raise ValueError('Attempt to add duplicate search term "%s"'%t) self._search_term_map[t] = key - def search_term_to_key(self, term): + def search_term_to_field_key(self, term): if term in self._search_term_map: return self._search_term_map[term] return term - def searchable_field_keys(self): + def searchable_fields(self): return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field' and len(self._tb_cats[k]['search_terms']) > 0] From ba6f2f0c5e6cd9943827cda2ccc4a59d32fdbb8b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 21 Sep 2010 18:50:21 +0100 Subject: [PATCH 4/4] Take out commit= parameter on set_authors and set_title. Change other code where necessary --- src/calibre/gui2/dialogs/metadata_bulk.py | 5 ++++- src/calibre/library/database2.py | 19 ++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 8a692d94d5..18d00191cc 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -484,7 +484,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): setter = self.db.set_comment else: setter = getattr(self.db, 'set_'+dest) - setter(id, val, notify=False, commit=False) + if dest in ['title', 'authors']: + setter(id, val, notify=False) + else: + setter(id, val, notify=False, commit=False) self.db.commit() dynamic['s_r_search_mode'] = self.search_mode.currentIndex() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 77e3afc8a3..1fdacfc09f 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -407,7 +407,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): path = path.lower() return path - def set_path(self, index, index_is_id=False, commit=True): + def set_path(self, index, index_is_id=False): ''' Set the path to the directory containing this books files based on its current title and author. If there was a previous directory, its contents @@ -447,8 +447,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.add_format(id, format, stream, index_is_id=True, path=tpath, notify=False) self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id)) - if commit: - self.conn.commit() + self.conn.commit() self.data.set(id, self.FIELD_MAP['path'], path, row_is_id=True) # Delete not needed directories if current_path and os.path.exists(spath): @@ -1212,7 +1211,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): result.append(r) return ' & '.join(result).replace('|', ',') - def set_authors(self, id, authors, notify=True, commit=True): + def set_authors(self, id, authors, notify=True): ''' `authors`: A list of authors. ''' @@ -1240,17 +1239,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ss = self.author_sort_from_book(id, index_is_id=True) self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) - if commit: - self.conn.commit() + self.conn.commit() self.data.set(id, self.FIELD_MAP['authors'], ','.join([a.replace(',', '|') for a in authors]), row_is_id=True) self.data.set(id, self.FIELD_MAP['author_sort'], ss, row_is_id=True) - self.set_path(id, index_is_id=True, commit=commit) + self.set_path(id, index_is_id=True) if notify: self.notify('metadata', [id]) - def set_title(self, id, title, notify=True, commit=True): + def set_title(self, id, title, notify=True): if not title: return if not isinstance(title, unicode): @@ -1261,9 +1259,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) else: self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) - self.set_path(id, index_is_id=True, commit=commit) - if commit: - self.conn.commit() + self.set_path(id, index_is_id=True) + self.conn.commit() if notify: self.notify('metadata', [id])