mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
commit
26f3d27fdb
@ -36,6 +36,13 @@ def cleanup_tags(tags):
|
|||||||
ans.append(tag)
|
ans.append(tag)
|
||||||
return ans
|
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):
|
class LibraryDatabase(object):
|
||||||
|
|
||||||
''' Emulate the old LibraryDatabase2 interface '''
|
''' Emulate the old LibraryDatabase2 interface '''
|
||||||
@ -58,9 +65,9 @@ class LibraryDatabase(object):
|
|||||||
self.is_second_db = is_second_db
|
self.is_second_db = is_second_db
|
||||||
self.listeners = set()
|
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,
|
read_only=read_only, restore_all_prefs=restore_all_prefs,
|
||||||
progress_callback=progress_callback)
|
progress_callback=progress_callback)
|
||||||
cache = self.new_api = Cache(backend)
|
cache = self.new_api = Cache(backend)
|
||||||
cache.init()
|
cache.init()
|
||||||
self.data = View(cache)
|
self.data = View(cache)
|
||||||
|
@ -43,6 +43,11 @@ class DevicePlugin(Plugin):
|
|||||||
#: than THUMBNAIL_HEIGHT
|
#: than THUMBNAIL_HEIGHT
|
||||||
# THUMBNAIL_WIDTH = 68
|
# 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
|
#: 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
|
#: sync_booklists. Setting it to true will ask device.py to refresh the
|
||||||
#: cover thumbnails during book matching
|
#: cover thumbnails during book matching
|
||||||
|
@ -202,8 +202,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
|||||||
# making this number effectively around 10 to 15 larger.
|
# making this number effectively around 10 to 15 larger.
|
||||||
PATH_FUDGE_FACTOR = 40
|
PATH_FUDGE_FACTOR = 40
|
||||||
|
|
||||||
THUMBNAIL_HEIGHT = 160
|
THUMBNAIL_HEIGHT = 160
|
||||||
DEFAULT_THUMBNAIL_HEIGHT = 160
|
DEFAULT_THUMBNAIL_HEIGHT = 160
|
||||||
|
THUMBNAIL_COMPRESSION_QUALITY = 70
|
||||||
|
|
||||||
PREFIX = ''
|
PREFIX = ''
|
||||||
BACKLOADING_ERROR_MESSAGE = None
|
BACKLOADING_ERROR_MESSAGE = None
|
||||||
@ -292,12 +293,22 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
|||||||
'particular IP address. The driver will listen only on the '
|
'particular IP address. The driver will listen only on the '
|
||||||
'entered address, and this address will be the one advertized '
|
'entered address, and this address will be the one advertized '
|
||||||
'over mDNS (bonjour).') + '</p>',
|
'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 '
|
_('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 '
|
'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 '
|
'book will not change even if the save template produces a '
|
||||||
'different result. Using this option in most cases prevents '
|
'different result. Using this option in most cases prevents '
|
||||||
'having multiple copies of a book on the device.') + '</p>',
|
'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 = [
|
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||||
False, '',
|
False, '',
|
||||||
@ -306,7 +317,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
|||||||
False, '',
|
False, '',
|
||||||
'', '',
|
'', '',
|
||||||
True, '',
|
True, '',
|
||||||
True
|
True, '70'
|
||||||
]
|
]
|
||||||
OPT_AUTOSTART = 0
|
OPT_AUTOSTART = 0
|
||||||
OPT_PASSWORD = 2
|
OPT_PASSWORD = 2
|
||||||
@ -317,6 +328,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
|||||||
OPT_AUTODISCONNECT = 10
|
OPT_AUTODISCONNECT = 10
|
||||||
OPT_FORCE_IP_ADDRESS = 11
|
OPT_FORCE_IP_ADDRESS = 11
|
||||||
OPT_OVERWRITE_BOOKS_UUID = 12
|
OPT_OVERWRITE_BOOKS_UUID = 12
|
||||||
|
OPT_COMPRESSION_QUALITY = 13
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
@ -1288,6 +1300,19 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
|||||||
self.client_can_stream_metadata = False
|
self.client_can_stream_metadata = False
|
||||||
self.client_wants_uuid_file_names = 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
|
message = None
|
||||||
try:
|
try:
|
||||||
self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
@ -9,8 +9,10 @@ import os
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from contextlib import closing
|
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.actions import InterfaceAction
|
||||||
from calibre.gui2 import (error_dialog, Dispatcher, warning_dialog, gprefs,
|
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.gui2.widgets import HistoryLineEdit
|
||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
from calibre.utils.date import now
|
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): # {{{
|
class Worker(Thread): # {{{
|
||||||
|
|
||||||
@ -47,11 +115,11 @@ class Worker(Thread): # {{{
|
|||||||
|
|
||||||
self.done()
|
self.done()
|
||||||
|
|
||||||
def add_formats(self, id, paths, newdb, replace=True):
|
def add_formats(self, id_, paths, newdb, replace=True):
|
||||||
for path in paths:
|
for path in paths:
|
||||||
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
|
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
|
||||||
with open(path, 'rb') as f:
|
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)
|
notify=False, replace=replace)
|
||||||
|
|
||||||
def doit(self):
|
def doit(self):
|
||||||
@ -174,6 +242,10 @@ class ChooseLibrary(QDialog): # {{{
|
|||||||
return (unicode(self.le.text()), self.delete_after_copy)
|
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):
|
class CopyToLibraryAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Copy To Library'
|
name = 'Copy To Library'
|
||||||
@ -231,6 +303,11 @@ class CopyToLibraryAction(InterfaceAction):
|
|||||||
_('Cannot copy to current library.'), show=True)
|
_('Cannot copy to current library.'), show=True)
|
||||||
self.copy_to_library(path, delete_after)
|
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):
|
def copy_to_library(self, loc, delete_after=False):
|
||||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
@ -252,6 +329,41 @@ class CopyToLibraryAction(InterfaceAction):
|
|||||||
self.pd.set_msg(title)
|
self.pd.set_msg(title)
|
||||||
self.pd.set_value(idx)
|
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),
|
self.worker = Worker(ids, db, loc, Dispatcher(progress),
|
||||||
Dispatcher(self.pd.accept), delete_after)
|
Dispatcher(self.pd.accept), delete_after)
|
||||||
self.worker.start()
|
self.worker.start()
|
||||||
|
@ -1233,7 +1233,8 @@ class DeviceMixin(object): # {{{
|
|||||||
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
|
||||||
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
|
||||||
try:
|
try:
|
||||||
return thumbnail(data, ht, ht)
|
return thumbnail(data, ht, ht,
|
||||||
|
compression_quality=self.device_manager.device.THUMBNAIL_COMPRESSION_QUALITY)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user