Show a warning when copy to library is use don incompatible libs

Show a warning when attempting to copy books between libraries that do
no have the same set of custom columns. Fixes #1225484 [New feature: copy books between libraries with all custom metadata](https://bugs.launchpad.net/calibre/+bug/1225484)

Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
Kovid Goyal 2013-09-16 11:43:03 +05:30
commit 26f3d27fdb
5 changed files with 161 additions and 11 deletions

View File

@ -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,7 +65,7 @@ class LibraryDatabase(object):
self.is_second_db = is_second_db
self.listeners = set()
backend = self.backend = DB(library_path, default_prefs=default_prefs,
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)

View File

@ -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

View File

@ -204,6 +204,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
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).') + '</p>',
_('Replace books with the same calibre identifier') + ':::<p>' +
_('Replace books with same calibre ID') + ':::<p>' +
_('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.') + '</p>',
_('Cover thumbnail compression quality') + ':::<p>' +
_('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.') + '</p>',
]
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)

View File

@ -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 <i>{0}</i> library are different from the '
'custom columns in the <i>{1}</i> 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 <i>{0}</i> 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()

View File

@ -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