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