From 7d77e8fe984c9a5dda5f14d386c489085acf4030 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 15 Sep 2013 14:24:17 +0200 Subject: [PATCH 1/3] When copying books from library A to library B, check that B contains the necessary custom columns. If not, tell the user and allow them to create any non-conflicting columns. Inform them about conflicting columns. --- src/calibre/gui2/actions/copy_to_library.py | 110 +++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py index b93efa1dc4..059fa036e3 100644 --- a/src/calibre/gui2/actions/copy_to_library.py +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -10,7 +10,8 @@ from functools import partial from threading import Thread from contextlib import closing -from PyQt4.Qt import (QToolButton, QDialog, QGridLayout, QIcon, QLabel, QDialogButtonBox) +from PyQt4.Qt import (QToolButton, QDialog, QGridLayout, QIcon, QLabel, QDialogButtonBox, + QFormLayout, QCheckBox) from calibre.gui2.actions import InterfaceAction from calibre.gui2 import (error_dialog, Dispatcher, warning_dialog, gprefs, @@ -19,6 +20,8 @@ from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.widgets import HistoryLineEdit from calibre.utils.config import prefs, tweaks from calibre.utils.date import now +from calibre.utils.icu import sort_key + class Worker(Thread): # {{{ @@ -47,11 +50,11 @@ class Worker(Thread): # {{{ self.done() - def add_formats(self, id, paths, newdb, replace=True): + def add_formats(self, id_, paths, newdb, replace=True): for path in paths: fmt = os.path.splitext(path)[-1].replace('.', '').upper() with open(path, 'rb') as f: - newdb.add_format(id, fmt, f, index_is_id=True, + newdb.add_format(id_, fmt, f, index_is_id=True, notify=False, replace=replace) def doit(self): @@ -174,6 +177,10 @@ class ChooseLibrary(QDialog): # {{{ return (unicode(self.le.text()), self.delete_after_copy) # }}} +# Static session-long set of pairs of libraries that have had their custom columns +# checked for compatibility +libraries_with_checked_columns = {} + class CopyToLibraryAction(InterfaceAction): name = 'Copy To Library' @@ -231,6 +238,11 @@ class CopyToLibraryAction(InterfaceAction): _('Cannot copy to current library.'), show=True) self.copy_to_library(path, delete_after) + def _column_is_compatible(self, source_metadata, dest_metadata): + return (source_metadata['datatype'] == dest_metadata['datatype'] and + (source_metadata['datatype'] != 'text' or + source_metadata['is_multiple'] == dest_metadata['is_multiple'])) + def copy_to_library(self, loc, delete_after=False): rows = self.gui.library_view.selectionModel().selectedRows() if not rows or len(rows) == 0: @@ -252,6 +264,39 @@ class CopyToLibraryAction(InterfaceAction): self.pd.set_msg(title) self.pd.set_value(idx) + # Open the new db so we can check the custom columns. + + global libraries_with_checked_columns + if db.library_id not in libraries_with_checked_columns: + libraries_with_checked_columns[db.library_id] = set() + + from calibre.db.legacy import LibraryDatabase + newdb = LibraryDatabase(loc, is_second_db=True) + + continue_processing = True; + with closing(newdb): + if newdb.library_id not in libraries_with_checked_columns[db.library_id]: + + newdb_meta = newdb.field_metadata.custom_field_metadata() + incompatible_columns = [] + missing_columns = [] + for k, m in db.field_metadata.custom_iteritems(): + if k not in newdb_meta: + missing_columns.append(k) + elif not self._column_is_compatible(m, newdb_meta[k]): + incompatible_columns.append(k) + + if missing_columns or incompatible_columns: + continue_processing = self.custom_column_dialog(db, newdb, + missing_columns, incompatible_columns) + if continue_processing: + libraries_with_checked_columns[db.library_id].add(newdb.library_id) + + newdb.break_cycles() + del newdb + if not continue_processing: + return; + self.worker = Worker(ids, db, loc, Dispatcher(progress), Dispatcher(self.pd.accept), delete_after) self.worker.start() @@ -295,4 +340,63 @@ class CopyToLibraryAction(InterfaceAction): _('You cannot use other libraries while using the environment' ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'), show=True) + def custom_column_dialog(self, db, newdb, missing_cols, incompatible_cols): + source_metadata = db.field_metadata.custom_field_metadata(include_composites=True) + d = QDialog(self.gui) + d.setWindowTitle(_('Create link')) + l = QFormLayout() + d.setLayout(l) + d.setMinimumWidth(600) + d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + + d.la = la = QLabel(_( + 'The custom columns in the source library are different from the ' + 'custom columns in the destination library. Incompatible columns ' + 'are columns with the same lookup key but different column ' + 'types. These cannot be copied. Missing columns are columns ' + 'in the source library but not in the destination library. ' + 'If you check the "Create" box, these columns will be added ' + 'to the destination and their values copied with the books.')) + la.setWordWrap(True) + la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') + l.setWidget(0, l.SpanningRole, la) + if incompatible_cols: + l.addRow(_('Incompatible custom columns:'), + QLabel(', '.join(sorted(incompatible_cols, key=sort_key)))) + + incompatible_cols_widgets = [] + if missing_cols: + l.addRow(QLabel(_('Missing custom columns')), QLabel('')) + for k in missing_cols: + widgets = (k, QCheckBox(_('Add column to new library'))) + l.addRow(QLabel(k), widgets[1]) + incompatible_cols_widgets.append(widgets) + + l.addRow(d.bb) + d.bb.accepted.connect(d.accept) + d.bb.rejected.connect(d.reject) + d.resize(d.sizeHint()) + if d.exec_() == d.Accepted: + count = 0 + for k,cb in incompatible_cols_widgets: + if cb.isChecked(): + count += 1 + if count: + pd = ProgressDialog(_('Creating custom columns'), min=0, max=count, + parent=self.gui, cancelable=False) + pd.show() + done_count = 0 + for k,cb in incompatible_cols_widgets: + if cb.isChecked(): + pd.set_value(done_count) + pd.set_msg(_('Creating column {0}').format(k)) + done_count += 1 + col_meta = source_metadata[k] + newdb.create_custom_column( + col_meta['label'], col_meta['name'], col_meta['datatype'], + len(col_meta['is_multiple']) > 0, + col_meta['is_editable'], col_meta['display']) + pd.done(0) + return True + return False \ No newline at end of file From d69211e3b1fce94873776fb4af8f3067109cb3e8 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 15 Sep 2013 19:07:47 +0200 Subject: [PATCH 2/3] Allow a device to determine the compression quality setting for thumbnail generation --- .../devices/smart_device_app/driver.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 33b3b6a74c..f98d624e55 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -202,8 +202,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): # making this number effectively around 10 to 15 larger. PATH_FUDGE_FACTOR = 40 - THUMBNAIL_HEIGHT = 160 - DEFAULT_THUMBNAIL_HEIGHT = 160 + THUMBNAIL_HEIGHT = 160 + DEFAULT_THUMBNAIL_HEIGHT = 160 + THUMBNAIL_COMPRESSION_QUALITY = 70 PREFIX = '' BACKLOADING_ERROR_MESSAGE = None @@ -292,12 +293,22 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): 'particular IP address. The driver will listen only on the ' 'entered address, and this address will be the one advertized ' 'over mDNS (bonjour).') + '

', - _('Replace books with the same calibre identifier') + ':::

' + + _('Replace books with same calibre ID') + ':::

' + _('Use this option to overwrite a book on the device if that book ' 'has the same calibre identifier as the book being sent. The file name of the ' 'book will not change even if the save template produces a ' 'different result. Using this option in most cases prevents ' 'having multiple copies of a book on the device.') + '

', + _('Cover thumbnail compression quality') + ':::

' + + _('Use this option to control the size and quality of the cover ' + 'file sent to the device. It must be between 50 and 99. ' + 'The larger the number the higher quality the cover, but also ' + 'the larger the file. For example, changing this from 70 to 90 ' + 'results in a much better cover that is approximately 2.5 ' + 'times as big. To see the changes you must force calibre ' + 'to resend metadata to the device, either by changing ' + 'the metadata for the book (updating the last modification ' + 'time) or resending the book itself.') + '

', ] EXTRA_CUSTOMIZATION_DEFAULT = [ False, '', @@ -306,7 +317,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): False, '', '', '', True, '', - True + True, '70' ] OPT_AUTOSTART = 0 OPT_PASSWORD = 2 @@ -317,6 +328,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): OPT_AUTODISCONNECT = 10 OPT_FORCE_IP_ADDRESS = 11 OPT_OVERWRITE_BOOKS_UUID = 12 + OPT_COMPRESSION_QUALITY = 13 def __init__(self, path): @@ -1288,6 +1300,19 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.client_can_stream_metadata = False self.client_wants_uuid_file_names = False + compression_quality_ok = True + try: + cq = int(self.settings().extra_customization[self.OPT_COMPRESSION_QUALITY]) + if cq < 50 or cq > 99: + compression_quality_ok = False + except: + 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' + self._debug(message) + return message + message = None try: self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From 72cf895392983274f900505d4310166fc2dbd4fc Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 15 Sep 2013 19:09:57 +0200 Subject: [PATCH 3/3] Commit the other two files --- src/calibre/devices/interface.py | 5 +++++ src/calibre/gui2/device.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 9b173b091e..920965ee6c 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -43,6 +43,11 @@ class DevicePlugin(Plugin): #: than THUMBNAIL_HEIGHT # THUMBNAIL_WIDTH = 68 + #: Compression quality for thumbnails. Set this closer to 100 to have better + #: quality thumbnails with fewer compression artifacts. Of course, the + #: thumbnails get larger as well. + THUMBNAIL_COMPRESSION_QUALITY=70 + #: Set this to True if the device supports updating cover thumbnails during #: sync_booklists. Setting it to true will ask device.py to refresh the #: cover thumbnails during book matching diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 77517b94f9..d97d44381e 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1233,7 +1233,8 @@ class DeviceMixin(object): # {{{ ht = self.device_manager.device.THUMBNAIL_HEIGHT \ if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT try: - return thumbnail(data, ht, ht) + return thumbnail(data, ht, ht, + compression_quality=self.device_manager.device.THUMBNAIL_COMPRESSION_QUALITY) except: pass