diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 1e7c7550f8..2082a3b90a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -565,6 +565,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ mb = self.library_view.model().metadata_backup if mb is not None: mb.stop() + # give the thread time to stop so all operations complete + # otherwise the exit could kill the thread mid-write + time.sleep(2) self.hide_windows() self.emailer.stop() diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 7a15cb3ce1..09adc4a9fd 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -39,6 +39,7 @@ class MetadataBackup(Thread): # {{{ self.do_write = FunctionDispatcher(self.write) self.get_metadata_for_dump = FunctionDispatcher(db.get_metadata_for_dump) self.clear_dirtied = FunctionDispatcher(db.clear_dirtied) + self.set_dirtied = FunctionDispatcher(db.dirtied) def stop(self): self.keep_running = False @@ -68,8 +69,9 @@ class MetadataBackup(Thread): # {{{ traceback.print_exc() continue + # at this point the dirty indication is off + if mi is None: - self.clear_dirtied([id_]) continue # Give the GUI thread a chance to do something. Python threads don't @@ -79,6 +81,7 @@ class MetadataBackup(Thread): # {{{ try: raw = metadata_to_opf(mi) except: + self.set_dirtied([id_]) prints('Failed to convert to opf for id:', id_) traceback.print_exc() continue @@ -92,16 +95,11 @@ class MetadataBackup(Thread): # {{{ try: self.do_write(path, raw) except: + self.set_dirtied([id_]) prints('Failed to write backup metadata for id:', id_, 'again, giving up') continue - time.sleep(0.1) # Give the GUI thread a chance to do something - try: - self.clear_dirtied([id_]) - except: - prints('Failed to clear dirtied for id:', id_) - def write(self, path, raw): with open(path, 'wb') as f: f.write(raw) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ce72b473e1..281f8cdc78 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -598,20 +598,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for book_id in book_ids: if not self.data.has_id(book_id): continue - path, mi = self.get_metadata_for_dump(book_id) + path, mi = self.get_metadata_for_dump(book_id, + remove_from_dirtied=remove_from_dirtied) if path is None: continue - raw = metadata_to_opf(mi) - with open(path, 'wb') as f: - f.write(raw) - if remove_from_dirtied: - self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?', - (book_id,)) - # if a later exception prevents the commit, then the dirtied - # table will still have the book. No big deal, because the OPF - # is there and correct. We will simply do it again on next - # start - self.dirtied_cache.discard(book_id) + try: + raw = metadata_to_opf(mi) + with open(path, 'wb') as f: + f.write(raw) + except: + # Something went wrong. Put the book back on the dirty list + self.dirtied([book_id]) if commit: self.conn.commit() return True @@ -649,7 +646,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.dirtied_cache = set() self.dirtied(book_ids) - def get_metadata_for_dump(self, idx): + def get_metadata_for_dump(self, idx, remove_from_dirtied=True): try: path = os.path.join(self.abspath(idx, index_is_id=True), 'metadata.opf') mi = self.get_metadata(idx, index_is_id=True) @@ -658,7 +655,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # cover is set/removed mi.cover = 'cover.jpg' except: - return (None, None) + # This almost certainly means that the book has been deleted while + # the backup operation sat in the queue. + path,mi = (None, None) + + try: + # clear the dirtied indicator. The user must put it back if + # something goes wrong with writing the OPF + if remove_from_dirtied: + self.clear_dirtied([idx]) + except: + # No real problem. We will just do it again. + pass return (path, mi) def get_metadata(self, idx, index_is_id=False, get_cover=False):