diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 3bc36dc8d5..64205173e8 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -36,6 +36,13 @@ def cleanup_tags(tags): ans.append(tag) return ans +def create_backend( + library_path, default_prefs=None, read_only=False, + progress_callback=lambda x, y:True, restore_all_prefs=False): + return DB(library_path, default_prefs=default_prefs, + read_only=read_only, restore_all_prefs=restore_all_prefs, + progress_callback=progress_callback) + class LibraryDatabase(object): ''' Emulate the old LibraryDatabase2 interface ''' @@ -58,9 +65,9 @@ class LibraryDatabase(object): self.is_second_db = is_second_db self.listeners = set() - backend = self.backend = DB(library_path, default_prefs=default_prefs, - read_only=read_only, restore_all_prefs=restore_all_prefs, - progress_callback=progress_callback) + backend = self.backend = create_backend(library_path, default_prefs=default_prefs, + read_only=read_only, restore_all_prefs=restore_all_prefs, + progress_callback=progress_callback) cache = self.new_api = Cache(backend) cache.init() self.data = View(cache) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 9b173b091e..b3ac714344 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 = 75 + #: 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/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) diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py index b93efa1dc4..a17abbc2fb 100644 --- a/src/calibre/gui2/actions/copy_to_library.py +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -9,8 +9,10 @@ import os from functools import partial from threading import Thread from contextlib import closing +from collections import defaultdict -from PyQt4.Qt import (QToolButton, QDialog, QGridLayout, QIcon, QLabel, QDialogButtonBox) +from PyQt4.Qt import (QToolButton, QDialog, QGridLayout, QIcon, QLabel, QDialogButtonBox, + QFormLayout, QCheckBox, QWidget, QScrollArea, QVBoxLayout) from calibre.gui2.actions import InterfaceAction from calibre.gui2 import (error_dialog, Dispatcher, warning_dialog, gprefs, @@ -19,6 +21,72 @@ 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 + +def ask_about_cc_mismatch(gui, db, newdb, missing_cols, incompatible_cols): # {{{ + source_metadata = db.field_metadata.custom_field_metadata(include_composites=True) + ndbname = os.path.basename(newdb.library_path) + + d = QDialog(gui) + d.setWindowTitle(_('Different custom columns')) + l = QFormLayout() + tl = QVBoxLayout() + d.setLayout(tl) + d.s = QScrollArea(d) + tl.addWidget(d.s) + d.w = QWidget(d) + d.s.setWidget(d.w) + d.s.setWidgetResizable(True) + d.w.setLayout(l) + d.setMinimumWidth(600) + d.setMinimumHeight(500) + d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + + msg = _('The custom columns in the {0} library are different from the ' + 'custom columns in the {1} library. As a result, some metadata might not be copied.').format( + os.path.basename(db.library_path), ndbname) + d.la = la = QLabel(msg) + la.setWordWrap(True) + la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') + l.addRow(la) + if incompatible_cols: + la = d.la2 = QLabel(_('The following columns are incompatible - they have the same name' + ' but different data types. They will be ignored: ') + + ', '.join(sorted(incompatible_cols, key=sort_key))) + la.setWordWrap(True) + la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') + l.addRow(la) + + missing_widgets = [] + if missing_cols: + la = d.la3 = QLabel(_('The following columns are missing in the {0} library.' + ' You can choose to add them automatically below.').format( + ndbname)) + la.setWordWrap(True) + l.addRow(la) + for k in missing_cols: + widgets = (k, QCheckBox(_('Add to the %s library') % ndbname)) + l.addRow(QLabel(k), widgets[1]) + missing_widgets.append(widgets) + d.la4 = la = QLabel(_('This warning is only shown once per library, per session')) + la.setWordWrap(True) + tl.addWidget(la) + + tl.addWidget(d.bb) + d.bb.accepted.connect(d.accept) + d.bb.rejected.connect(d.reject) + d.resize(d.sizeHint()) + if d.exec_() == d.Accepted: + for k, cb in missing_widgets: + if cb.isChecked(): + 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']) + return True + return False +# }}} class Worker(Thread): # {{{ @@ -47,11 +115,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 +242,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 = defaultdict(set) + class CopyToLibraryAction(InterfaceAction): name = 'Copy To Library' @@ -231,6 +303,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 +329,41 @@ class CopyToLibraryAction(InterfaceAction): self.pd.set_msg(title) self.pd.set_value(idx) + # Open the new db so we can check the custom columns. We use only the + # backend since we only need the custom column definitions, not the + # rest of the data in the db. + + global libraries_with_checked_columns + + from calibre.db.legacy import create_backend + newdb = create_backend(loc) + + 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 m['datatype'] == 'composite': + continue + 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 = ask_about_cc_mismatch(self.gui, db, newdb, + missing_columns, incompatible_columns) + if continue_processing: + libraries_with_checked_columns[db.library_id].add(newdb.library_id) + + newdb.close() + del newdb + if not continue_processing: + return + self.worker = Worker(ids, db, loc, Dispatcher(progress), Dispatcher(self.pd.accept), delete_after) self.worker.start() 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