diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index e5979c3b59..3fa9698615 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -726,7 +726,7 @@ class DevicePlugin(Plugin): ''' return False - def synchronize_with_db(self, db, book_id, book_metadata): + def synchronize_with_db(self, db, book_id, book_metadata, first_call): ''' Called during book matching when a book on the device is matched with a book in calibre's db. The method is responsible for syncronizing @@ -740,23 +740,28 @@ class DevicePlugin(Plugin): This is useful when the calire data is correct but must be sent to the device. - The second value in the tuple specifies whether a book format should be - sent to the device. The intent is to permit verifying that the book on - the device is the same as the book in calibre. Return None if no book is - to be sent, otherwise return the base file name on the device (a string - like foobar.epub). Be sure to include the extension in the name. The - device subsystem will construct a send_books job for all books with not- - None returned values. Note: other than to later retrieve the extension, - the name is ignored in cases where the device uses a template to - generate the file name, which most do. + The second value is itself a 2-value tuple. The first value in the tuple + specifies whether a book format should be sent to the device. The intent + is to permit verifying that the book on the device is the same as the + book in calibre. This value must be None if no book is to be sent, + otherwise return the base file name on the device (a string like + foobar.epub). Be sure to include the extension in the name. The device + subsystem will construct a send_books job for all books with not- None + returned values. Note: other than to later retrieve the extension, the + name is ignored in cases where the device uses a template to generate + the file name, which most do. The second value in the returned tuple + indicated whether the format is future-dated. Return True if it is, + otherwise return False. Calibre will display a dialog to the user + listing all future dated books. Extremely important: this method is called on the GUI thread. It must be threadsafe with respect to the device manager's thread. book_id: the calibre id for the book in the database. book_metadata: the Metadata object for the book coming from the device. + first_call: True if this is the first call during a sync, False otherwise ''' - return None, None + return (None, (None, False)) class BookList(list): ''' diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index b2ab262323..0836df3807 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -1546,36 +1546,49 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): def specialize_global_preferences(self, device_prefs): device_prefs.set_overrides(manage_device_metadata='on_connect') + def _show_message(self, message): + self._call_client("DISPLAY_MESSAGE", + {'messageKind': self.MESSAGE_SHOW_TOAST, + 'message': message}) + def _check_if_format_send_needed(self, db, id_, book): if not self.will_ask_for_update_books: return None - from calibre.utils.date import parse_date, now, isoformat + from calibre.utils.date import parse_date, isoformat try: if not hasattr(book, '_format_mtime_'): return None - cc_mtime = parse_date(book.get('_format_mtime_'), as_utc=False) ext = posixpath.splitext(book.lpath)[1][1:] fmt_metadata = db.new_api.format_metadata(id_, ext) if fmt_metadata: calibre_mtime = fmt_metadata['mtime'] + if calibre_mtime > self.now: + if not self.have_sent_future_dated_book_message: + self.have_sent_future_dated_book_message = True + self._show_message(_('You have book formats in your library ' + 'with dates in the future. See calibre ' + 'for details')) + return (None, True) + + cc_mtime = parse_date(book.get('_format_mtime_'), as_utc=False) self._debug(book.title, 'cal_mtime', calibre_mtime, 'cc_mtime', cc_mtime) if cc_mtime < calibre_mtime: - book.set('_format_mtime_', isoformat(now())) - return posixpath.basename(book.lpath) + book.set('_format_mtime_', isoformat(self.now)) + return (posixpath.basename(book.lpath), False) except: self._debug('exception checking if must send format', book.title) traceback.print_exc() - return None + return (None, False) @synchronous('sync_lock') - def synchronize_with_db(self, db, id_, book): - from calibre.utils.date import parse_date, is_date_undefined - def show_message(message): - self._call_client("DISPLAY_MESSAGE", - {'messageKind': self.MESSAGE_SHOW_TOAST, - 'message': message}) + def synchronize_with_db(self, db, id_, book, first_call): + from calibre.utils.date import parse_date, is_date_undefined, now + + if first_call: + self.have_sent_future_dated_book_message = False + self.now = now() if self.have_bad_sync_columns or not (self.is_read_sync_col or self.is_read_date_sync_col): @@ -1589,24 +1602,24 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): if self.is_read_sync_col: if self.is_read_sync_col not in fm: self._debug('is_read_sync_col not in field_metadata') - show_message(_("The read sync column %s is " + self._show_message(_("The read sync column %s is " "not in calibre's library")%self.is_read_sync_col) self.have_bad_sync_columns = True elif fm[self.is_read_sync_col]['datatype'] != 'bool': self._debug('is_read_sync_col not bool type') - show_message(_("The read sync column %s is " + self._show_message(_("The read sync column %s is " "not a Yes/No column")%self.is_read_sync_col) self.have_bad_sync_columns = True if self.is_read_date_sync_col: if self.is_read_date_sync_col not in fm: self._debug('is_read_date_sync_col not in field_metadata') - show_message(_("The read date sync column %s is " + self._show_message(_("The read date sync column %s is " "not in calibre's library")%self.is_read_date_sync_col) self.have_bad_sync_columns = True elif fm[self.is_read_date_sync_col]['datatype'] != 'datetime': self._debug('is_read_date_sync_col not date type') - show_message(_("The read date sync column %s is " + self._show_message(_("The read date sync column %s is " "not a Date column")%self.is_read_date_sync_col) self.have_bad_sync_columns = True @@ -1786,6 +1799,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.is_read_date_sync_col = None self.have_checked_sync_columns = False self.have_bad_sync_columns = False + self.have_sent_future_dated_book_message = False + self.now = None message = None compression_quality_ok = True diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index bb416f3399..956f14c3d4 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1776,6 +1776,8 @@ class DeviceMixin(object): # {{{ book_ids_to_refresh = set() book_formats_to_send = [] + books_with_future_dates = [] + self._first_call_to_synchronize_with_db = True def update_book(id_, book) : if not update_metadata: @@ -1791,9 +1793,13 @@ class DeviceMixin(object): # {{{ return False if self.device_manager.device is not None: - set_of_ids, fmt_name = \ - self.device_manager.device.synchronize_with_db(db, id_, book) - if fmt_name is not None: + set_of_ids, (fmt_name, date_bad) = \ + self.device_manager.device.synchronize_with_db(db, id_, book, + self._first_call_to_synchronize_with_db) + self._first_call_to_synchronize_with_db = False + if date_bad: + books_with_future_dates.append(book.title) + elif fmt_name is not None: book_formats_to_send.append((id_, fmt_name)) if set_of_ids is not None: book_ids_to_refresh.update(set_of_ids) @@ -1946,6 +1952,19 @@ class DeviceMixin(object): # {{{ # Shouldn't ever happen, but just in case traceback.print_exc() + # Inform user about future-dated books + try: + if books_with_future_dates: + d = error_dialog(self, _('Book format sync problem'), + _('Some book formats in your library cannot be ' + 'synced because they have dates in the future'), + det_msg='\n'.join(books_with_future_dates), + show=False, + show_copy_button=True) + d.show() + except: + traceback.print_exc() + if DEBUG: prints('DeviceJob: set_books_in_library finished: time=', time.time() - start_time)