From a4777f81e97c7b82862d5836465ae7e8134ee3b1 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 16 Jan 2014 09:46:04 +0100 Subject: [PATCH 1/3] Make setting an incorrect compression quality value non-fatal by forcing incorrect values to the default. --- src/calibre/devices/smart_device_app/driver.py | 12 ++++++++---- src/calibre/gui2/dialogs/smartdevice.py | 2 +- src/calibre/gui2/ui.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 208db0ce75..95fea47d20 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -208,6 +208,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): THUMBNAIL_HEIGHT = 160 DEFAULT_THUMBNAIL_HEIGHT = 160 THUMBNAIL_COMPRESSION_QUALITY = 75 + DEFAULT_THUMBNAIL_COMPRESSION_QUALITY = 75 PREFIX = '' BACKLOADING_ERROR_MESSAGE = None @@ -680,6 +681,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): return self.OPT_PORT_NUMBER elif opt_string == 'force_ip_address': return self.OPT_FORCE_IP_ADDRESS + elif opt_string == 'thumbnail_compression_quality': + return self.OPT_COMPRESSION_QUALITY else: return None @@ -1463,6 +1466,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.connection_attempts = {} self.client_wants_uuid_file_names = False + message = None compression_quality_ok = True try: cq = int(self.settings().extra_customization[self.OPT_COMPRESSION_QUALITY]) @@ -1474,11 +1478,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): compression_quality_ok = False if not compression_quality_ok: self.THUMBNAIL_COMPRESSION_QUALITY = 70 - message = 'Bad compression quality setting. It must be a number between 50 and 99' + message = _('Bad compression quality setting. It must be a number ' + 'between 50 and 99. Forced to be %d.')%self.DEFAULT_THUMBNAIL_COMPRESSION_QUALITY self._debug(message) - return message + self.set_option('thumbnail_compression_quality', + str(self.DEFAULT_THUMBNAIL_COMPRESSION_QUALITY)) - message = None try: self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) set_socket_inherit(self.listen_socket, False) @@ -1541,7 +1546,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): # Now try to open a UDP socket to receive broadcasts on - message = None try: self.broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) except: diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index bebec4abee..9bd570a12e 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -143,7 +143,7 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): if not self.device_manager.is_running('smartdevice'): error_dialog(self, _('Problem starting the wireless device'), - _('The wireless device driver did not start. It said "%s"')%message, + _('The wireless device driver had problems starting. It said "%s"')%message, show=True) self.device_manager.set_option('smartdevice', 'use_fixed_port', self.orig_fixed_port) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index a019ae11b6..8888ba7a07 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -432,7 +432,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if message: if not self.device_manager.is_running('Wireless Devices'): error_dialog(self, _('Problem starting the wireless device'), - _('The wireless device driver did not start. ' + _('The wireless device driver had problems starting. ' 'It said "%s"')%message, show=True) self.iactions['Connect Share'].set_smartdevice_action_state() From 4b88f78bd0279ee8677a28d71a17767da04ef002 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 16 Jan 2014 12:36:53 +0100 Subject: [PATCH 2/3] Book matching during device connection: 1) add some debugging output 2) add some user feedback that it is happening by changing the status bar and setting a wait cursor. 3) make the book matching during device connection pseudo-multithread by processing GUI events during matching. In particular, this prevents the OS from deciding that calibre has stopped responding. --- src/calibre/gui2/device.py | 49 +++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 1200df2bcf..e1a574d1d5 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -8,7 +8,8 @@ from threading import Thread, Event from PyQt4.Qt import ( QMenu, QAction, QActionGroup, QIcon, SIGNAL, Qt, pyqtSignal, QDialog, - QObject, QVBoxLayout, QDialogButtonBox, QCursor) + QObject, QVBoxLayout, QDialogButtonBox, QCursor, QCoreApplication, + QApplication) from calibre.customize.ui import (available_input_formats, available_output_formats, device_plugins, disabled_device_plugins) @@ -1092,8 +1093,14 @@ class DeviceMixin(object): # {{{ self.device_job_exception(job) return self.device_manager.slow_driveinfo() + # set_books_in_library might schedule a sync_booklists job + if DEBUG: + prints('DeviceJob: metadata_downloaded: Starting set_books_in_library') self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job) + + if DEBUG: + prints('DeviceJob: metadata_downloaded: updating views') mainlist, cardalist, cardblist = job.result self.memory_view.set_database(mainlist) self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA, @@ -1107,9 +1114,17 @@ class DeviceMixin(object): # {{{ self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA, self.device_manager.device.BACKLOADING_ERROR_MESSAGE is None) + if DEBUG: + prints('DeviceJob: metadata_downloaded: syncing') self.sync_news() self.sync_catalogs() + + if DEBUG: + prints('DeviceJob: metadata_downloaded: refreshing ondevice') self.refresh_ondevice() + + if DEBUG: + prints('DeviceJob: metadata_downloaded: sending metadata_available signal') device_signals.device_metadata_available.emit() def refresh_ondevice(self, reset_only=False): @@ -1766,8 +1781,35 @@ class DeviceMixin(object): # {{{ except: return True + total_book_count = 0; for booklist in booklists: for book in booklist: + if book: + total_book_count += 1; + if DEBUG: + prints('DeviceJob: set_books_in_library: books to process=', total_book_count) + + start_time = time.time() + QApplication.setOverrideCursor(Qt.WaitCursor) + current_book_count = 0; + for booklist in booklists: + for book in booklist: + if current_book_count % 100 == 0: + self.status_bar.show_message( + _('Analyzing books on the device: %d%% finished')%( + int((float(current_book_count)/total_book_count)*100.0))) + + # I am assuming that this pseudo multi-threading won't break + # anything. Reasons: UI actions that change the DB will happen + # synchronously with this loop, and the device booklist isn't + # used until this loop finishes. Of course, the UI will stutter + # somewhat, but that is better than locking up. Why every tenth + # book? WAG balancing performance in the loop with the user + # being able to get something done + if current_book_count % 10 == 0: + QCoreApplication.processEvents() + + current_book_count += 1; book.in_library = None if getattr(book, 'uuid', None) in self.db_book_uuid_cache: id_ = db_book_uuid_cache[book.uuid] @@ -1831,6 +1873,11 @@ class DeviceMixin(object): # {{{ self.device_manager.sync_booklists( FunctionDispatcher(self.metadata_synced), booklists, plugboards, add_as_step_to_job) + + QApplication.restoreOverrideCursor() + if DEBUG: + prints('DeviceJob: set_books_in_library finished: time=', time.time() - start_time) + # The status line is reset when the job finishes return update_metadata # }}} From 7aa33052ac815594e7ba95ee48445a6eb3404597 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Thu, 16 Jan 2014 13:08:28 +0100 Subject: [PATCH 3/3] Only accept non-user and non-network events while processing books. --- src/calibre/gui2/device.py | 170 +++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 81 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index e1a574d1d5..9d4f252574 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -9,7 +9,7 @@ from threading import Thread, Event from PyQt4.Qt import ( QMenu, QAction, QActionGroup, QIcon, SIGNAL, Qt, pyqtSignal, QDialog, QObject, QVBoxLayout, QDialogButtonBox, QCursor, QCoreApplication, - QApplication) + QApplication, QEventLoop) from calibre.customize.ui import (available_input_formats, available_output_formats, device_plugins, disabled_device_plugins) @@ -1790,93 +1790,101 @@ class DeviceMixin(object): # {{{ prints('DeviceJob: set_books_in_library: books to process=', total_book_count) start_time = time.time() + QApplication.setOverrideCursor(Qt.WaitCursor) - current_book_count = 0; - for booklist in booklists: - for book in booklist: - if current_book_count % 100 == 0: - self.status_bar.show_message( - _('Analyzing books on the device: %d%% finished')%( - int((float(current_book_count)/total_book_count)*100.0))) + try: + current_book_count = 0; + for booklist in booklists: + for book in booklist: + if current_book_count % 100 == 0: + self.status_bar.show_message( + _('Analyzing books on the device: %d%% finished')%( + int((float(current_book_count)/total_book_count)*100.0))) - # I am assuming that this pseudo multi-threading won't break - # anything. Reasons: UI actions that change the DB will happen - # synchronously with this loop, and the device booklist isn't - # used until this loop finishes. Of course, the UI will stutter - # somewhat, but that is better than locking up. Why every tenth - # book? WAG balancing performance in the loop with the user - # being able to get something done - if current_book_count % 10 == 0: - QCoreApplication.processEvents() - - current_book_count += 1; - book.in_library = None - if getattr(book, 'uuid', None) in self.db_book_uuid_cache: - id_ = db_book_uuid_cache[book.uuid] - if updateq(id_, book): - update_book(id_, book) - book.in_library = 'UUID' - # ensure that the correct application_id is set - book.application_id = id_ - continue - # No UUID exact match. Try metadata matching. - book_title = clean_string(book.title) - d = self.db_book_title_cache.get(book_title, None) - if d is not None: - # At this point we know that the title matches. The book - # will match if any of the db_id, author, or author_sort - # also match. - if getattr(book, 'application_id', None) in d['db_ids']: - id_ = getattr(book, 'application_id', None) - update_book(id_, book) - book.in_library = 'APP_ID' - # app_id already matches a db_id. No need to set it. - continue - # Sonys know their db_id independent of the application_id - # in the metadata cache. Check that as well. - if getattr(book, 'db_id', None) in d['db_ids']: - update_book(book.db_id, book) - book.in_library = 'DB_ID' - book.application_id = book.db_id - continue - # We now know that the application_id is not right. Set it - # to None to prevent book_on_device from accidentally - # matching on it. It will be set to a correct value below if - # the book is matched with one in the library - book.application_id = None - if book.authors: - # Compare against both author and author sort, because - # either can appear as the author - book_authors = clean_string(authors_to_string(book.authors)) - if book_authors in d['authors']: - id_ = d['authors'][book_authors] + # I am assuming that this sort-of multi-threading won't break + # anything. Reasons: excluding UI events prevents the user + # from explicitly changing anything, and (in theory) no + # changes are happening because of timers and the like. + # Why every tenth book? WAG balancing performance in the + # loop with preventing App Not Responding errors + if current_book_count % 10 == 0: + QCoreApplication.processEvents(flags= + QEventLoop.ExcludeUserInputEvents| + QEventLoop.ExcludeSocketNotifiers) + time.sleep(0.01) + current_book_count += 1; + book.in_library = None + if getattr(book, 'uuid', None) in self.db_book_uuid_cache: + id_ = db_book_uuid_cache[book.uuid] + if updateq(id_, book): update_book(id_, book) - book.in_library = 'AUTHOR' - book.application_id = id_ - elif book_authors in d['author_sort']: - id_ = d['author_sort'][book_authors] + book.in_library = 'UUID' + # ensure that the correct application_id is set + book.application_id = id_ + continue + # No UUID exact match. Try metadata matching. + book_title = clean_string(book.title) + d = self.db_book_title_cache.get(book_title, None) + if d is not None: + # At this point we know that the title matches. The book + # will match if any of the db_id, author, or author_sort + # also match. + if getattr(book, 'application_id', None) in d['db_ids']: + id_ = getattr(book, 'application_id', None) update_book(id_, book) - book.in_library = 'AUTH_SORT' - book.application_id = id_ - else: - # Book definitely not matched. Clear its application ID - book.application_id = None - # Set author_sort if it isn't already - asort = getattr(book, 'author_sort', None) - if not asort and book.authors: - book.author_sort = self.library_view.model().db.\ - author_sort_from_authors(book.authors) + book.in_library = 'APP_ID' + # app_id already matches a db_id. No need to set it. + continue + # Sonys know their db_id independent of the application_id + # in the metadata cache. Check that as well. + if getattr(book, 'db_id', None) in d['db_ids']: + update_book(book.db_id, book) + book.in_library = 'DB_ID' + book.application_id = book.db_id + continue + # We now know that the application_id is not right. Set it + # to None to prevent book_on_device from accidentally + # matching on it. It will be set to a correct value below if + # the book is matched with one in the library + book.application_id = None + if book.authors: + # Compare against both author and author sort, because + # either can appear as the author + book_authors = clean_string(authors_to_string(book.authors)) + if book_authors in d['authors']: + id_ = d['authors'][book_authors] + update_book(id_, book) + book.in_library = 'AUTHOR' + book.application_id = id_ + elif book_authors in d['author_sort']: + id_ = d['author_sort'][book_authors] + update_book(id_, book) + book.in_library = 'AUTH_SORT' + book.application_id = id_ + else: + # Book definitely not matched. Clear its application ID + book.application_id = None + # Set author_sort if it isn't already + asort = getattr(book, 'author_sort', None) + if not asort and book.authors: + book.author_sort = self.library_view.model().db.\ + author_sort_from_authors(book.authors) - if update_metadata: - if self.device_manager.is_device_connected: - plugboards = self.library_view.model().db.prefs.get('plugboards', {}) - self.device_manager.sync_booklists( - FunctionDispatcher(self.metadata_synced), booklists, - plugboards, add_as_step_to_job) + if update_metadata: + if self.device_manager.is_device_connected: + plugboards = self.library_view.model().db.prefs.get('plugboards', {}) + self.device_manager.sync_booklists( + FunctionDispatcher(self.metadata_synced), booklists, + plugboards, add_as_step_to_job) + except: + traceback.print_exc() + raise + finally: + QApplication.restoreOverrideCursor() - QApplication.restoreOverrideCursor() if DEBUG: - prints('DeviceJob: set_books_in_library finished: time=', time.time() - start_time) + prints('DeviceJob: set_books_in_library finished: time=', + time.time() - start_time) # The status line is reset when the job finishes return update_metadata # }}}