From 66d498e7b349939c8349f2d9194884d4c148be6c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 5 Jul 2010 13:16:05 +0100 Subject: [PATCH 1/4] First iteration of automatic metadata updating --- src/calibre/devices/usbms/books.py | 13 +++++++-- src/calibre/devices/usbms/driver.py | 2 +- src/calibre/gui2/device.py | 25 ++++++++++++---- src/calibre/gui2/dialogs/config/add_save.py | 17 +++++++---- src/calibre/gui2/dialogs/config/add_save.ui | 32 ++++++++++++++++----- src/calibre/gui2/library/models.py | 2 +- src/calibre/gui2/library/views.py | 2 +- src/calibre/utils/config.py | 4 +-- 8 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 42d0f3c863..6394626a9f 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -132,6 +132,8 @@ class CollectionsBookList(BookList): return True def get_collections(self, collection_attributes): + from calibre.devices.usbms.driver import debug_print + debug_print('Starting get_collections:', prefs['manage_device_metadata']) collections = {} series_categories = set([]) # This map of sets is used to avoid linear searches when testing for @@ -146,14 +148,19 @@ class CollectionsBookList(BookList): # book in all existing collections. Do not add any new ones. attrs = ['device_collections'] if getattr(book, '_new_book', False): - if prefs['preserve_user_collections']: + if prefs['manage_device_metadata'] == 'manual': # Ensure that the book is in all the book's existing # collections plus all metadata collections attrs += collection_attributes else: - # The book's existing collections are ignored. Put the book - # in collections defined by its metadata. + # For new books, both 'on_send' and 'on_connect' do the same + # thing. The book's existing collections are ignored. Put + # the book in collections defined by its metadata. attrs = collection_attributes + elif prefs['manage_device_metadata'] == 'on_connect': + # For existing books, modify the collections only if the user + # specified 'on_connect' + attrs = collection_attributes for attr in attrs: attr = attr.strip() val = getattr(book, attr, None) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 377ec36c16..1a088cc77c 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -58,7 +58,7 @@ class USBMS(CLI, Device): debug_print ('USBMS: Fetching list of books from device. oncard=', oncard) - dummy_bl = BookList(None, None, None) + dummy_bl = booklist_class(None, None, None) if oncard == 'carda' and not self._card_a_prefix: self.report_progress(1.0, _('Getting list of books on device...')) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 4acde6089b..d976fad128 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -33,6 +33,7 @@ from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import DEBUG +from calibre.utils.config import prefs # }}} @@ -1425,19 +1426,24 @@ class DeviceMixin(object): # {{{ aus = re.sub('(?u)\W|[_]', '', aus) self.db_book_title_cache[title]['author_sort'][aus] = mi self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi - self.db_book_uuid_cache[mi.uuid] = mi.application_id + self.db_book_uuid_cache[mi.uuid] = mi # Now iterate through all the books on the device, setting the # in_library field Fastest and most accurate key is the uuid. Second is # the application_id, which is really the db key, but as this can # accidentally match across libraries we also verify the title. The # db_id exists on Sony devices. Fallback is title and author match + + update_metadata = prefs['manage_device_metadata'] == 'on_connect' for booklist in booklists: for book in booklist: if getattr(book, 'uuid', None) in self.db_book_uuid_cache: + if update_metadata: + book.smart_update(self.db_book_uuid_cache[book.uuid]) book.in_library = True # ensure that the correct application_id is set - book.application_id = self.db_book_uuid_cache[book.uuid] + book.application_id = \ + self.db_book_uuid_cache[book.uuid].application_id continue book_title = book.title.lower() if book.title else '' @@ -1447,11 +1453,13 @@ class DeviceMixin(object): # {{{ if d is not None: if getattr(book, 'application_id', None) in d['db_ids']: book.in_library = True - book.smart_update(d['db_ids'][book.application_id]) + if update_metadata: + book.smart_update(d['db_ids'][book.application_id]) continue if book.db_id in d['db_ids']: book.in_library = True - book.smart_update(d['db_ids'][book.db_id]) + if update_metadata: + book.smart_update(d['db_ids'][book.db_id]) continue if book.authors: # Compare against both author and author sort, because @@ -1460,14 +1468,19 @@ class DeviceMixin(object): # {{{ book_authors = re.sub('(?u)\W|[_]', '', book_authors) if book_authors in d['authors']: book.in_library = True - book.smart_update(d['authors'][book_authors]) + if update_metadata: + book.smart_update(d['authors'][book_authors]) elif book_authors in d['author_sort']: book.in_library = True - book.smart_update(d['author_sort'][book_authors]) + if update_metadata: + book.smart_update(d['author_sort'][book_authors]) # 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: + self.device_manager.sync_booklists(None, booklists) # }}} diff --git a/src/calibre/gui2/dialogs/config/add_save.py b/src/calibre/gui2/dialogs/config/add_save.py index b1f5621f44..8eb6cf7bd0 100644 --- a/src/calibre/gui2/dialogs/config/add_save.py +++ b/src/calibre/gui2/dialogs/config/add_save.py @@ -45,7 +45,12 @@ class AddSave(QTabWidget, Ui_TabWidget): self.metadata_box.layout().insertWidget(0, self.filename_pattern) self.opt_swap_author_names.setChecked(prefs['swap_author_names']) self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing']) - self.preserve_user_collections.setChecked(prefs['preserve_user_collections']) + if prefs['manage_device_metadata'] == 'manual': + self.manage_device_metadata.setCurrentIndex(0) + elif prefs['manage_device_metadata'] == 'on_send': + self.manage_device_metadata.setCurrentIndex(1) + else: + self.manage_device_metadata.setCurrentIndex(2) help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75)) self.save_template.initialize('save_to_disk', opts.template, help) self.send_template.initialize('send_to_device', opts.send_template, help) @@ -72,12 +77,14 @@ class AddSave(QTabWidget, Ui_TabWidget): prefs['filename_pattern'] = pattern prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked()) prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked()) - prefs['preserve_user_collections'] = bool(self.preserve_user_collections.isChecked()) - + if self.manage_device_metadata.currentIndex() == 0: + prefs['manage_device_metadata'] = 'manual' + elif self.manage_device_metadata.currentIndex() == 1: + prefs['manage_device_metadata'] = 'on_send' + else: + prefs['manage_device_metadata'] = 'on_connect' return True - - if __name__ == '__main__': from PyQt4.Qt import QApplication app=QApplication([]) diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui index 64a8137aa1..35824ef847 100644 --- a/src/calibre/gui2/dialogs/config/add_save.ui +++ b/src/calibre/gui2/dialogs/config/add_save.ui @@ -14,7 +14,7 @@ TabWidget - 0 + 2 @@ -179,16 +179,34 @@ Title match ignores leading indefinite articles ("the", "a", - - - Preserve device collections. - + + + + Manual management. Calibre updates cached metadata and adds collections, but never removes them + + + + + Calibre manages metadata when sending books. Calibre updates cached metadata, and adds/remove collections + + + + + Calibre manages metadata at device connection. Calibre updates cached metadata, and adds/removes collections + + - If checked, collections will not be deleted even if a book with changed metadata is resent and the collection is not in the book's metadata. In addition, editing collections in the device view will be enabled. If unchecked, collections will be always reflect only the metadata in the calibre library. + Choose 'Manual Management', to have Calibre update the metadata cache (not the book) and add collections when a book is sent. With this option, calibre will never remove a collection. Choose 'Calibre manages metadata when sending books' to have Calibre update the metadata cache and add/remove collections when you send a book to the device. Choose 'Calibre manages metadata when device is connected' to have Calibre update the metadata cache and add/remove collections when you connect the device. + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true @@ -198,7 +216,7 @@ Title match ignores leading indefinite articles ("the", "a", - + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 7ffbc42f02..147aa1188d 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -939,7 +939,7 @@ class DeviceBooksModel(BooksModel): # {{{ (cname == 'collections' and \ callable(getattr(self.db, 'supports_collections', None)) and \ self.db.supports_collections() and \ - prefs['preserve_user_collections']): + prefs['manage_device_metadata']=='manual'): flags |= Qt.ItemIsEditable return flags diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 9d85dce075..c6c32f86f7 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -503,7 +503,7 @@ class DeviceBooksView(BooksView): # {{{ self.edit_collections_menu.setVisible( callable(getattr(self._model.db, 'supports_collections', None)) and \ self._model.db.supports_collections() and \ - prefs['preserve_user_collections']) + prefs['manage_device_metadata'] == 'manual') self.context_menu.popup(event.globalPos()) event.accept() diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index f24a6d2e30..5c4bd55644 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -698,8 +698,8 @@ def _prefs(): # calibre server can execute searches c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) - c.add_opt('preserve_user_collections', default=True, - help=_('Preserve all collections even if not in library metadata.')) + c.add_opt('manage_device_metadata', default='manual', + help=_('How and when calibre updates metadata on the device.')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c From 25905711fb0fea3454c0a6dd97422c5d2cc2fbfe Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 5 Jul 2010 14:48:51 +0100 Subject: [PATCH 2/4] 1) More changes for connect-time syncing. 2) some changes to the Boox driver --- src/calibre/devices/hanlin/driver.py | 17 ++++++++++------- src/calibre/devices/usbms/driver.py | 4 +++- src/calibre/gui2/dialogs/config/add_save.ui | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index f19135a7e7..3db55d05c5 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -119,22 +119,25 @@ class BOOX(HANLINV3): MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory' STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card' - EBOOK_DIR_MAIN = ['MyBooks'] - EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' - 'send e-books to on the device. The first one that exists will ' - 'be used.') + EBOOK_DIR_MAIN = ['MyBooks', 'MyBooks'] + EXTRA_CUSTOMIZATION_MESSAGE = _('Directories to send e-books to on the device. ' + 'The first directory is used for main memory. ' + 'The second is used for cards.') EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) - # EBOOK_DIR_CARD_A = 'MyBooks' ## Am quite sure we need this. - def post_open_callback(self): opts = self.settings() dirs = opts.extra_customization if not dirs: dirs = self.EBOOK_DIR_MAIN else: + # will have at least one item ... dirs = [x.strip() for x in dirs.split(',')] - self.EBOOK_DIR_MAIN = dirs + self.EBOOK_DIR_MAIN = dirs[0] + if len(dirs) > 1: + self.EBOOK_DIR_CARD_A = self.EBOOK_DIR_CARD_B = dirs[1] + else: + self.EBOOK_DIR_CARD_A = self.EBOOK_DIR_CARD_B = dirs[0] def windows_sort_drives(self, drives): return drives diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 1a088cc77c..73a329be58 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -58,7 +58,7 @@ class USBMS(CLI, Device): debug_print ('USBMS: Fetching list of books from device. oncard=', oncard) - dummy_bl = booklist_class(None, None, None) + dummy_bl = self.booklist_class(None, None, None) if oncard == 'carda' and not self._card_a_prefix: self.report_progress(1.0, _('Getting list of books on device...')) @@ -78,6 +78,8 @@ class USBMS(CLI, Device): self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \ self.get_main_ebook_dir() + debug_print ('USBMS: dirs are:', prefix, ebook_dirs) + # get the metadata cache bl = self.booklist_class(oncard, prefix, self.settings) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui index 35824ef847..9ded5d91c2 100644 --- a/src/calibre/gui2/dialogs/config/add_save.ui +++ b/src/calibre/gui2/dialogs/config/add_save.ui @@ -6,7 +6,7 @@ 0 0 - 588 + 599 516 @@ -200,7 +200,7 @@ Title match ignores leading indefinite articles ("the", "a", - Choose 'Manual Management', to have Calibre update the metadata cache (not the book) and add collections when a book is sent. With this option, calibre will never remove a collection. Choose 'Calibre manages metadata when sending books' to have Calibre update the metadata cache and add/remove collections when you send a book to the device. Choose 'Calibre manages metadata when device is connected' to have Calibre update the metadata cache and add/remove collections when you connect the device. + Choose 'Manual Management', to have Calibre update the metadata cache (not the book) and add collections when a book is sent. With this option, calibre will never remove a collection. Choose 'Calibre manages metadata when sending books' to have Calibre update the metadata cache and add/remove collections for a book when it is sent to the device. Choose 'Calibre manages metadata when device is connected' to have Calibre update the metadata cache and add/remove collections when calibre connects to the device and when you send a book. Qt::PlainText From 0b4e09180dfd8d443a7fa1a9ebbae4806ca834c2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 8 Jul 2010 18:30:19 +0100 Subject: [PATCH 3/4] Revert boox driver --- src/calibre/devices/hanlin/driver.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index 3db55d05c5..f19135a7e7 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -119,25 +119,22 @@ class BOOX(HANLINV3): MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory' STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card' - EBOOK_DIR_MAIN = ['MyBooks', 'MyBooks'] - EXTRA_CUSTOMIZATION_MESSAGE = _('Directories to send e-books to on the device. ' - 'The first directory is used for main memory. ' - 'The second is used for cards.') + EBOOK_DIR_MAIN = ['MyBooks'] + EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' + 'send e-books to on the device. The first one that exists will ' + 'be used.') EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) + # EBOOK_DIR_CARD_A = 'MyBooks' ## Am quite sure we need this. + def post_open_callback(self): opts = self.settings() dirs = opts.extra_customization if not dirs: dirs = self.EBOOK_DIR_MAIN else: - # will have at least one item ... dirs = [x.strip() for x in dirs.split(',')] - self.EBOOK_DIR_MAIN = dirs[0] - if len(dirs) > 1: - self.EBOOK_DIR_CARD_A = self.EBOOK_DIR_CARD_B = dirs[1] - else: - self.EBOOK_DIR_CARD_A = self.EBOOK_DIR_CARD_B = dirs[0] + self.EBOOK_DIR_MAIN = dirs def windows_sort_drives(self, drives): return drives From 0a7171d7b7bfef8ba7657569dc840bf96ff7411b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 10 Jul 2010 06:40:53 +0100 Subject: [PATCH 4/4] Change search restriction box to use grey help text. Remove label. --- src/calibre/gui2/main.ui | 17 +++---- src/calibre/gui2/search_restriction_mixin.py | 7 +-- src/calibre/gui2/widgets.py | 52 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index d89a451cda..0d937527f6 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -123,17 +123,7 @@ 0 - - - &Restrict to: - - - search_restriction - - - - - + 150 @@ -543,6 +533,11 @@ QComboBox
calibre.gui2.search_box
+ + ComboBoxWithHelp + QComboBox +
calibre.gui2.widgets
+
ThrobbingButton QToolButton diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 3a71fa3de0..6f31f65c13 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -7,7 +7,8 @@ Created on 10 Jun 2010 class SearchRestrictionMixin(object): def __init__(self): - self.search_restriction.activated[str].connect(self.apply_search_restriction) + self.search_restriction.initialize(help_text=_('Restrict To')) + self.search_restriction.activated[int].connect(self.apply_search_restriction) self.library_view.model().count_changed_signal.connect(self.restriction_count_changed) self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) self.search_restriction.setMinimumContentsLength(10) @@ -27,8 +28,8 @@ class SearchRestrictionMixin(object): if self.restriction_in_effect: self.set_number_of_books_shown() - def apply_search_restriction(self, r): - r = unicode(r) + def apply_search_restriction(self, i): + r = unicode(self.search_restriction.currentText()) if r is not None and r != '': self.restriction_in_effect = True restriction = 'search:"%s"'%(r) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index d94d8e7292..63be2b19ec 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -731,6 +731,58 @@ class HistoryLineEdit(QComboBox): def text(self): return self.currentText() +class ComboBoxWithHelp(QComboBox): + ''' + A combobox where item 0 is help text. CurrentText will return '' for item 0. + Be sure to always fetch the text with currentText. Don't use the signals + that pass a string, because they will not correct the text. + ''' + def __init__(self, parent=None): + QComboBox.__init__(self, parent) + self.normal_background = 'rgb(255, 255, 255, 0%)' + self.connect(self, SIGNAL('currentIndexChanged(int)'), self.index_changed) + self.help_text = '' + self.state_set = False + + def initialize(self, help_text=_('Search')): + self.help_text = help_text + self.set_state() + + def set_state(self): + if not self.state_set: + if self.currentIndex() == 0: + self.setItemText(0, self.help_text) + self.setStyleSheet( + 'QComboBox { color: gray; background-color: %s; }' % + self.normal_background) + else: + self.setItemText(0, '') + self.setStyleSheet( + 'QComboBox { color: black; background-color: %s; }' % + self.normal_background) + + def index_changed(self, index): + self.state_set = False + self.set_state() + + def currentText(self): + if self.currentIndex() == 0: + return '' + return QComboBox.currentText(self) + + def itemText(self, idx): + if idx == 0: + return '' + return QComboBox.itemText(self, idx) + + def showPopup(self): + self.setItemText(0, '') + QComboBox.showPopup(self) + + def hidePopup(self): + QComboBox.hidePopup(self) + self.set_state() + class PythonHighlighter(QSyntaxHighlighter): Rules = []