-
+ 0
@@ -342,56 +342,7 @@ Default: ~,Catalog
-
-
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 118
-
-
-
-
-
-
-
-
-
- ...
-
-
-
-
-
-
- ...
-
-
-
-
-
-
- ...
-
-
-
-
-
-
- ...
-
-
-
-
-
-
+
diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py
index ee43d21357..a7da345dc2 100644
--- a/src/calibre/library/catalogs/epub_mobi.py
+++ b/src/calibre/library/catalogs/epub_mobi.py
@@ -47,28 +47,44 @@ class EPUB_MOBI(CatalogPlugin):
"of the conversion process a bug is occurring.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
- Option('--exclude-book-marker',
- default=':',
- dest='exclude_book_marker',
- action = None,
- help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n"
- "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n"
- "Default: '%default'\n"
- "Applies to ePub, MOBI output formats")),
Option('--exclude-genre',
default='\[.+\]',
dest='exclude_genre',
action = None,
help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n"
"Applies to: ePub, MOBI output formats")),
- Option('--exclude-tags',
- default=('~,'+_('Catalog')),
- dest='exclude_tags',
- action = None,
- help=_("Comma-separated list of tag words indicating book should be excluded from output. "
- "For example: 'skip' will match 'skip this book' and 'Skip will like this'. "
- "Default: '%default'\n"
- "Applies to: ePub, MOBI output formats")),
+
+# Option('--exclude-book-marker',
+# default=':',
+# dest='exclude_book_marker',
+# action = None,
+# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n"
+# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n"
+# "Default: '%default'\n"
+# "Applies to ePub, MOBI output formats")),
+# Option('--exclude-tags',
+# default=('~,Catalog'),
+# dest='exclude_tags',
+# action = None,
+# help=_("Comma-separated list of tag words indicating book should be excluded from output. "
+# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. "
+# "Default:'%default'\n"
+# "Applies to: ePub, MOBI output formats")),
+
+ Option('--exclusion-rules',
+ default="(('Excluded tags','Tags','~,Catalog'),)",
+ dest='exclusion_rules',
+ action=None,
+ help=_("Specifies the rules used to exclude books from the generated catalog.\n"
+ "The model for an exclusion rule is either\n('','Tags','') or\n"
+ "('','','').\n"
+ "For example:\n"
+ "(('Archived books','#status','Archived'),)\n"
+ "will exclude a book with a value of 'Archived' in the custom column 'status'.\n"
+ "When multiple rules are defined, all rules will be applied.\n"
+ "Default: \n" + '"' + '%default' + '"' + "\n"
+ "Applies to ePub, MOBI output formats")),
+
Option('--generate-authors',
default=False,
dest='generate_authors',
@@ -142,7 +158,7 @@ class EPUB_MOBI(CatalogPlugin):
help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n"
"The model for a prefix rule is ('','','','').\n"
"When multiple rules are defined, the first matching rule will be used.\n"
- "Default: '%default'\n"
+ "Default:\n" + '"' + '%default' + '"' + "\n"
"Applies to ePub, MOBI output formats")),
Option('--thumb-width',
default='1.0',
@@ -285,6 +301,17 @@ class EPUB_MOBI(CatalogPlugin):
if len(rule) != 4:
log.error("incorrect number of args for --prefix-rules: %s" % repr(rule))
+ # eval exclusion_rules if passed from command line
+ if type(opts.exclusion_rules) is not tuple:
+ try:
+ opts.exclusion_rules = eval(opts.exclusion_rules)
+ except:
+ log.error("malformed --exclusion-rules: %s" % opts.exclusion_rules)
+ raise
+ for rule in opts.exclusion_rules:
+ if len(rule) != 3:
+ log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule))
+
# Display opts
keys = opts_dict.keys()
keys.sort()
@@ -292,6 +319,7 @@ class EPUB_MOBI(CatalogPlugin):
for key in keys:
if key in ['catalog_title','authorClip','connected_kindle','descriptionClip',
'exclude_book_marker','exclude_genre','exclude_tags',
+ 'exclusion_rules',
'header_note_source_field','merge_comments',
'output_profile','prefix_rules','read_book_marker',
'search_text','sort_by','sort_descriptions_by_author','sync',
From c080004682416308769663c289e0487c1922d806 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Aug 2012 19:51:45 +0200
Subject: [PATCH 12/27] Silly bug in smart device flood control
---
src/calibre/devices/smart_device_app/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index 4fe815dc80..b88c97a279 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -586,7 +586,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# the device.
raise OpenFailed('')
try:
- peer = self.device_socket.getpeername()
+ peer = self.device_socket.getpeername()[0]
self.connection_attempts[peer] = 0
except:
pass
From af3f6a62d9d1c27d3cba7a748aad0bab633d6364 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 6 Aug 2012 11:30:15 +0530
Subject: [PATCH 13/27] MTP driver: Get storage information
---
src/calibre/devices/interface.py | 2 +-
src/calibre/devices/mtp/unix/driver.py | 77 +++++++++++++++++-
src/calibre/devices/mtp/unix/libmtp.c | 104 ++++++++++++++++++++++---
3 files changed, 166 insertions(+), 17 deletions(-)
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index fc4e5a2a60..4fd5fea252 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -297,7 +297,7 @@ class DevicePlugin(Plugin):
:return: (device name, device version, software version on device, mime type)
The tuple can optionally have a fifth element, which is a
- drive information diction. See usbms.driver for an example.
+ drive information dictionary. See usbms.driver for an example.
"""
raise NotImplementedError()
diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py
index 081b09975f..91acae8a9d 100644
--- a/src/calibre/devices/mtp/unix/driver.py
+++ b/src/calibre/devices/mtp/unix/driver.py
@@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import time
+import time, operator
from threading import RLock
from functools import wraps
@@ -33,6 +33,11 @@ class MTP_DEVICE(MTPDeviceBase):
self.lock = RLock()
self.blacklisted_devices = set()
+ @synchronous
+ def get_gui_name(self):
+ if self.dev is None or not self.dev.friendly_name: return self.name
+ return self.dev.friendly_name
+
@synchronous
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
@@ -62,6 +67,10 @@ class MTP_DEVICE(MTPDeviceBase):
def post_yank_cleanup(self):
self.dev = None
+ @synchronous
+ def shutdown(self):
+ self.dev = None
+
@synchronous
def open(self, connected_device, library_uuid):
def blacklist_device():
@@ -69,18 +78,78 @@ class MTP_DEVICE(MTPDeviceBase):
self.blacklisted_devices.add((d.busnum, d.devnum, d.vendor_id,
d.product_id, d.bcd, d.serial))
try:
- self.detect.create_device(connected_device)
+ self.dev = self.detect.create_device(connected_device)
except ValueError:
# Give the device some time to settle
time.sleep(2)
try:
- self.detect.create_device(connected_device)
+ self.dev = self.detect.create_device(connected_device)
except ValueError:
# Black list this device so that it is ignored for the
# remainder of this session.
blacklist_device()
- raise OpenFailed('%s is not a MTP device'%connected_device)
+ raise OpenFailed('%s is not a MTP device'%(connected_device,))
except TypeError:
blacklist_device()
raise OpenFailed('')
+ storage = sorted(self.dev.storage_info, key=operator.itemgetter('id'))
+ if not storage:
+ blacklist_device()
+ raise OpenFailed('No storage found for device %s'%(connected_device,))
+ self._main_id = storage[0]['id']
+ self._carda_id = self._cardb_id = None
+ if len(storage) > 1:
+ self._carda_id = storage[1]['id']
+ if len(storage) > 2:
+ self._cardb_id = storage[2]['id']
+
+ @synchronous
+ def get_device_information(self, end_session=True):
+ d = self.dev
+ return (d.friendly_name, d.device_version, d.device_version, '')
+
+ @synchronous
+ def card_prefix(self, end_session=True):
+ ans = [None, None]
+ if self._carda_id is not None:
+ ans[0] = 'mtp:%d:'%self._carda_id
+ if self._cardb_id is not None:
+ ans[1] = 'mtp:%d:'%self._cardb_id
+ return tuple(ans)
+
+ @synchronous
+ def total_space(self, end_session=True):
+ ans = [0, 0, 0]
+ for s in self.dev.storage_info:
+ i = {self._main_id:0, self._carda_id:1,
+ self._cardb_id:2}.get(s['id'], None)
+ if i is not None:
+ ans[i] = s['capacity']
+ return tuple(ans)
+
+ @synchronous
+ def free_space(self, end_session=True):
+ self.dev.update_storage_info()
+ ans = [0, 0, 0]
+ for s in self.dev.storage_info:
+ i = {self._main_id:0, self._carda_id:1,
+ self._cardb_id:2}.get(s['id'], None)
+ if i is not None:
+ ans[i] = s['freespace_bytes']
+ return tuple(ans)
+
+
+if __name__ == '__main__':
+ from pprint import pprint
+ dev = MTP_DEVICE(None)
+ from calibre.devices.scanner import linux_scanner
+ devs = linux_scanner()
+ mtp_devs = dev.detect(devs)
+ dev.open(list(mtp_devs)[0], 'xxx')
+ d = dev.dev
+ print ("Opened device:", dev.get_gui_name())
+ print ("Storage info:")
+ pprint(d.storage_info)
+ print("Free space:", dev.free_space())
+
diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c
index 0a818cfc66..d22f294157 100644
--- a/src/calibre/devices/mtp/unix/libmtp.c
+++ b/src/calibre/devices/mtp/unix/libmtp.c
@@ -6,6 +6,24 @@
#include "devices.h"
+#define ENSURE_DEV(rval) \
+ if (self->device == NULL) { \
+ PyErr_SetString(PyExc_ValueError, "This device has not been initialized."); \
+ return rval; \
+ }
+
+// Storage types
+#define ST_Undefined 0x0000
+#define ST_FixedROM 0x0001
+#define ST_RemovableROM 0x0002
+#define ST_FixedRAM 0x0003
+#define ST_RemovableRAM 0x0004
+
+// Storage Access capability
+#define AC_ReadWrite 0x0000
+#define AC_ReadOnly 0x0001
+#define AC_ReadOnly_with_Object_Deletion 0x0002
+
// Device object definition {{{
typedef struct {
PyObject_HEAD
@@ -20,6 +38,7 @@ typedef struct {
} libmtp_Device;
+// Device.__init__() {{{
static void
libmtp_Device_dealloc(libmtp_Device* self)
{
@@ -119,44 +138,100 @@ libmtp_Device_init(libmtp_Device *self, PyObject *args, PyObject *kwds)
return 0;
}
+// }}}
-// Collator.friendly_name {{{
+// Device.friendly_name {{{
static PyObject *
libmtp_Device_friendly_name(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->friendly_name);
+ Py_INCREF(self->friendly_name); return self->friendly_name;
} // }}}
-// Collator.manufacturer_name {{{
+// Device.manufacturer_name {{{
static PyObject *
libmtp_Device_manufacturer_name(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->manufacturer_name);
+ Py_INCREF(self->manufacturer_name); return self->manufacturer_name;
} // }}}
-// Collator.model_name {{{
+// Device.model_name {{{
static PyObject *
libmtp_Device_model_name(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->model_name);
+ Py_INCREF(self->model_name); return self->model_name;
} // }}}
-// Collator.serial_number {{{
+// Device.serial_number {{{
static PyObject *
libmtp_Device_serial_number(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->serial_number);
+ Py_INCREF(self->serial_number); return self->serial_number;
} // }}}
-// Collator.device_version {{{
+// Device.device_version {{{
static PyObject *
libmtp_Device_device_version(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->device_version);
+ Py_INCREF(self->device_version); return self->device_version;
} // }}}
-// Collator.ids {{{
+// Device.ids {{{
static PyObject *
libmtp_Device_ids(libmtp_Device *self, void *closure) {
- return Py_BuildValue("O", self->ids);
+ Py_INCREF(self->ids); return self->ids;
+} // }}}
+
+// Device.update_storage_info() {{{
+static PyObject*
+libmtp_Device_update_storage_info(libmtp_Device *self, PyObject *args, PyObject *kwargs) {
+ ENSURE_DEV(NULL);
+ if (LIBMTP_Get_Storage(self->device, LIBMTP_STORAGE_SORTBY_NOTSORTED) < 0) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get storage infor for device.");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+// }}}
+
+// Device.storage_info {{{
+static PyObject *
+libmtp_Device_storage_info(libmtp_Device *self, void *closure) {
+ PyObject *ans, *loc;
+ LIBMTP_devicestorage_t *storage;
+ ENSURE_DEV(NULL);
+ if (self->device->storage == NULL) { PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); return NULL; }
+
+ ans = PyList_New(0);
+ if (ans == NULL) { PyErr_NoMemory(); return NULL; }
+
+ for (storage = self->device->storage; storage != NULL; storage = storage->next) {
+ // Ignore read only storage
+ if (storage->StorageType == ST_FixedROM || storage->StorageType == ST_RemovableROM) continue;
+ // Storage IDs with the lower 16 bits 0x0000 are not supposed to be
+ // writeable.
+ if ((storage->id & 0x0000FFFFU) == 0x00000000U) continue;
+ // Also check the access capability to avoid e.g. deletable only storages
+ if (storage->AccessCapability == AC_ReadOnly || storage->AccessCapability == AC_ReadOnly_with_Object_Deletion) continue;
+
+ loc = Py_BuildValue("{s:I,s:O,s:K,s:K,s:K,s:s,s:s}",
+ "id", storage->id,
+ "removable", ((storage->StorageType == ST_RemovableRAM) ? Py_True : Py_False),
+ "capacity", storage->MaxCapacity,
+ "freespace_bytes", storage->FreeSpaceInBytes,
+ "freespace_objects", storage->FreeSpaceInObjects,
+ "storage_desc", storage->StorageDescription,
+ "volume_id", storage->VolumeIdentifier
+ );
+
+ if (loc == NULL) return NULL;
+ if (PyList_Append(ans, loc) != 0) return NULL;
+ Py_DECREF(loc);
+
+ }
+
+ return ans;
} // }}}
static PyMethodDef libmtp_Device_methods[] = {
+ {"update_storage_info", (PyCFunction)libmtp_Device_update_storage_info, METH_VARARGS,
+ "update_storage_info() -> Reread the storage info from the device (total, space, free space, storage locations, etc.)"
+ },
+
{NULL} /* Sentinel */
};
@@ -191,6 +266,11 @@ static PyGetSetDef libmtp_Device_getsetters[] = {
(char *)"The ids of the device (busnum, devnum, vendor_id, product_id, usb_serialnum)",
NULL},
+ {(char *)"storage_info",
+ (getter)libmtp_Device_storage_info, NULL,
+ (char *)"Information about the storage locations on the device. Returns a list of dictionaries where each dictionary corresponds to the LIBMTP_devicestorage_struct.",
+ NULL},
+
{NULL} /* Sentinel */
};
From 4c64613a4dd7a323f75af2c1ee91dc64d28ce12b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 6 Aug 2012 11:52:51 +0530
Subject: [PATCH 14/27] ...
---
src/calibre/devices/interface.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 4fd5fea252..64aff3bad2 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -607,7 +607,7 @@ class BookList(list):
pass
def supports_collections(self):
- ''' Return True if the the device supports collections for this book list. '''
+ ''' Return True if the device supports collections for this book list. '''
raise NotImplementedError()
def add_book(self, book, replace_metadata):
From 498c9ba98cd860dc0e829c69fb34c2687b8aa151 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 6 Aug 2012 17:02:43 +0530
Subject: [PATCH 15/27] MTP driver: get files and folders on device
---
src/calibre/devices/mtp/base.py | 10 +-
src/calibre/devices/mtp/unix/driver.py | 12 ++
src/calibre/devices/mtp/unix/libmtp.c | 169 ++++++++++++++++++++++++-
3 files changed, 185 insertions(+), 6 deletions(-)
diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py
index 04dd2a0034..f29f525b30 100644
--- a/src/calibre/devices/mtp/base.py
+++ b/src/calibre/devices/mtp/base.py
@@ -8,9 +8,8 @@ __copyright__ = '2012, Kovid Goyal '
__docformat__ = 'restructuredtext en'
from calibre.devices.interface import DevicePlugin
-from calibre.devices.usbms.deviceconfig import DeviceConfig
-class MTPDeviceBase(DeviceConfig, DevicePlugin):
+class MTPDeviceBase(DevicePlugin):
name = 'SmartDevice App Interface'
gui_name = _('MTP Device')
icon = I('devices/galaxy_s3.png')
@@ -28,7 +27,14 @@ class MTPDeviceBase(DeviceConfig, DevicePlugin):
BACKLOADING_ERROR_MESSAGE = None
+ def __init__(self, *args, **kwargs):
+ DevicePlugin.__init__(self, *args, **kwargs)
+ self.progress_reporter = None
+
def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None):
pass
+ def set_progress_reporter(self, report_progress):
+ self.progress_reporter = report_progress
+
diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py
index 91acae8a9d..b8d6854fe5 100644
--- a/src/calibre/devices/mtp/unix/driver.py
+++ b/src/calibre/devices/mtp/unix/driver.py
@@ -33,6 +33,14 @@ class MTP_DEVICE(MTPDeviceBase):
self.lock = RLock()
self.blacklisted_devices = set()
+ def report_progress(self, sent, total):
+ try:
+ p = int(sent/total * 100)
+ except ZeroDivisionError:
+ p = 100
+ if self.progress_reporter is not None:
+ self.progress_reporter(p)
+
@synchronous
def get_gui_name(self):
if self.dev is None or not self.dev.friendly_name: return self.name
@@ -152,4 +160,8 @@ if __name__ == '__main__':
print ("Storage info:")
pprint(d.storage_info)
print("Free space:", dev.free_space())
+ files, errs = d.get_filelist(dev)
+ pprint((len(files), errs))
+ folders, errs = d.get_folderlist()
+ pprint((len(folders), errs))
diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c
index d22f294157..f748954936 100644
--- a/src/calibre/devices/mtp/unix/libmtp.c
+++ b/src/calibre/devices/mtp/unix/libmtp.c
@@ -6,12 +6,19 @@
#include "devices.h"
+// Macros and utilities
#define ENSURE_DEV(rval) \
if (self->device == NULL) { \
PyErr_SetString(PyExc_ValueError, "This device has not been initialized."); \
return rval; \
}
+#define ENSURE_STORAGE(rval) \
+ if (self->device->storage == NULL) { \
+ PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); \
+ return rval; \
+ }
+
// Storage types
#define ST_Undefined 0x0000
#define ST_FixedROM 0x0001
@@ -24,6 +31,41 @@
#define AC_ReadOnly 0x0001
#define AC_ReadOnly_with_Object_Deletion 0x0002
+typedef struct {
+ PyObject *obj;
+ PyThreadState *state;
+} ProgressCallback;
+
+static int report_progress(uint64_t const sent, uint64_t const total, void const *const data) {
+ PyObject *res;
+ ProgressCallback *cb;
+
+ cb = (ProgressCallback *)data;
+ if (cb->obj != NULL) {
+ PyEval_RestoreThread(cb->state);
+ res = PyObject_CallMethod(cb->obj, "report_progress", "KK", sent, total);
+ Py_XDECREF(res);
+ cb->state = PyEval_SaveThread();
+ }
+ return 0;
+}
+
+static void dump_errorstack(LIBMTP_mtpdevice_t *dev, PyObject *list) {
+ LIBMTP_error_t *stack;
+ PyObject *err;
+
+ for(stack = LIBMTP_Get_Errorstack(dev); stack != NULL; stack=stack->next) {
+ err = Py_BuildValue("Is", stack->errornumber, stack->error_text);
+ if (err == NULL) break;
+ PyList_Append(list, err);
+ Py_DECREF(err);
+ }
+
+ LIBMTP_Clear_Errorstack(dev);
+}
+
+// }}}
+
// Device object definition {{{
typedef struct {
PyObject_HEAD
@@ -88,7 +130,9 @@ libmtp_Device_init(libmtp_Device *self, PyObject *args, PyObject *kwds)
}
}
- dev = LIBMTP_Open_Raw_Device_Uncached(&rawdev);
+ // Note that contrary to what the libmtp docs imply, we cannot use
+ // LIBMTP_Open_Raw_Device_Uncached as using it causes file listing to fail
+ dev = LIBMTP_Open_Raw_Device(&rawdev);
Py_END_ALLOW_THREADS;
if (dev == NULL) {
@@ -193,8 +237,7 @@ static PyObject *
libmtp_Device_storage_info(libmtp_Device *self, void *closure) {
PyObject *ans, *loc;
LIBMTP_devicestorage_t *storage;
- ENSURE_DEV(NULL);
- if (self->device->storage == NULL) { PyErr_SetString(PyExc_RuntimeError, "The device has no storage information."); return NULL; }
+ ENSURE_DEV(NULL); ENSURE_STORAGE(NULL);
ans = PyList_New(0);
if (ans == NULL) { PyErr_NoMemory(); return NULL; }
@@ -208,7 +251,7 @@ libmtp_Device_storage_info(libmtp_Device *self, void *closure) {
// Also check the access capability to avoid e.g. deletable only storages
if (storage->AccessCapability == AC_ReadOnly || storage->AccessCapability == AC_ReadOnly_with_Object_Deletion) continue;
- loc = Py_BuildValue("{s:I,s:O,s:K,s:K,s:K,s:s,s:s}",
+ loc = Py_BuildValue("{s:k,s:O,s:K,s:K,s:K,s:s,s:s}",
"id", storage->id,
"removable", ((storage->StorageType == ST_RemovableRAM) ? Py_True : Py_False),
"capacity", storage->MaxCapacity,
@@ -227,11 +270,129 @@ libmtp_Device_storage_info(libmtp_Device *self, void *closure) {
return ans;
} // }}}
+// Device.get_filelist {{{
+static PyObject *
+libmtp_Device_get_filelist(libmtp_Device *self, PyObject *args, PyObject *kwargs) {
+ PyObject *ans, *fo, *callback = NULL, *errs;
+ ProgressCallback cb;
+ LIBMTP_file_t *f, *tf;
+
+ ENSURE_DEV(NULL); ENSURE_STORAGE(NULL);
+
+
+ if (!PyArg_ParseTuple(args, "|O", &callback)) return NULL;
+ cb.obj = callback;
+
+ ans = PyList_New(0);
+ errs = PyList_New(0);
+ if (ans == NULL || errs == NULL) { PyErr_NoMemory(); return NULL; }
+
+ cb.state = PyEval_SaveThread();
+ tf = LIBMTP_Get_Filelisting_With_Callback(self->device, report_progress, &cb);
+ PyEval_RestoreThread(cb.state);
+
+ if (tf == NULL) {
+ dump_errorstack(self->device, errs);
+ return Py_BuildValue("NN", ans, errs);
+ }
+
+ for (f=tf; f != NULL; f=f->next) {
+ fo = Py_BuildValue("{s:k,s:k,s:k,s:s,s:K,s:k}",
+ "id", f->item_id,
+ "parent_id", f->parent_id,
+ "storage_id", f->storage_id,
+ "filename", f->filename,
+ "size", f->filesize,
+ "modtime", f->modificationdate
+ );
+ if (fo == NULL || PyList_Append(ans, fo) != 0) break;
+ Py_DECREF(fo);
+ }
+
+ // Release memory
+ f = tf;
+ while (f != NULL) {
+ tf = f; f = f->next; LIBMTP_destroy_file_t(tf);
+ }
+
+ if (callback != NULL) {
+ // Bug in libmtp where it does not call callback with 100%
+ fo = PyObject_CallMethod(callback, "report_progress", "KK", PyList_Size(ans), PyList_Size(ans));
+ Py_XDECREF(fo);
+ }
+
+ return Py_BuildValue("NN", ans, errs);
+} // }}}
+
+// Device.get_folderlist {{{
+
+int folderiter(LIBMTP_folder_t *f, PyObject *parent) {
+ PyObject *folder, *children;
+
+ children = PyList_New(0);
+ if (children == NULL) { PyErr_NoMemory(); return 1;}
+
+ folder = Py_BuildValue("{s:k,s:k,s:k,s:s,s:N}",
+ "id", f->folder_id,
+ "parent_d", f->parent_id,
+ "storage_id", f->storage_id,
+ "name", f->name,
+ "children", children);
+ if (folder == NULL) return 1;
+ PyList_Append(parent, folder);
+ Py_DECREF(folder);
+
+ if (f->sibling != NULL) {
+ if (folderiter(f->sibling, parent)) return 1;
+ }
+
+ if (f->child != NULL) {
+ if (folderiter(f->child, children)) return 1;
+ }
+
+ return 0;
+}
+
+static PyObject *
+libmtp_Device_get_folderlist(libmtp_Device *self, PyObject *args, PyObject *kwargs) {
+ PyObject *ans, *errs;
+ LIBMTP_folder_t *f;
+
+ ENSURE_DEV(NULL); ENSURE_STORAGE(NULL);
+
+ ans = PyList_New(0);
+ errs = PyList_New(0);
+ if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; }
+
+ Py_BEGIN_ALLOW_THREADS;
+ f = LIBMTP_Get_Folder_List(self->device);
+ Py_END_ALLOW_THREADS;
+
+ if (f == NULL) {
+ dump_errorstack(self->device, errs);
+ return Py_BuildValue("NN", ans, errs);
+ }
+
+ if (folderiter(f, ans)) return NULL;
+ LIBMTP_destroy_folder_t(f);
+
+ return Py_BuildValue("NN", ans, errs);
+
+} // }}}
+
static PyMethodDef libmtp_Device_methods[] = {
{"update_storage_info", (PyCFunction)libmtp_Device_update_storage_info, METH_VARARGS,
"update_storage_info() -> Reread the storage info from the device (total, space, free space, storage locations, etc.)"
},
+ {"get_filelist", (PyCFunction)libmtp_Device_get_filelist, METH_VARARGS,
+ "get_filelist(callback=None) -> Get the list of files on the device. callback must be an object that has a method named 'report_progress(current, total)'. Returns files, errors."
+ },
+
+ {"get_folderlist", (PyCFunction)libmtp_Device_get_folderlist, METH_VARARGS,
+ "get_folderlist() -> Get the list of folders on the device. Returns files, erros."
+ },
+
{NULL} /* Sentinel */
};
From 24aab4fb5795e557ab1468a693907e310edfdd25 Mon Sep 17 00:00:00 2001
From: GRiker
Date: Mon, 6 Aug 2012 06:17:18 -0600
Subject: [PATCH 16/27] exclusion_rules_table added
---
src/calibre/gui2/catalog/catalog_epub_mobi.py | 249 +++++++++++++++---
src/calibre/gui2/catalog/catalog_epub_mobi.ui | 190 +++----------
src/calibre/library/catalogs/epub_mobi.py | 22 +-
.../library/catalogs/epub_mobi_builder.py | 93 +++++--
4 files changed, 315 insertions(+), 239 deletions(-)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index ac251ff801..8a2035c76f 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -38,7 +38,7 @@ class PluginWidget(QWidget,Ui_Form):
self._initControlArrays()
def _initControlArrays(self):
-
+ # Default values for controls
CheckBoxControls = []
ComboBoxControls = []
DoubleSpinBoxControls = []
@@ -72,13 +72,27 @@ class PluginWidget(QWidget,Ui_Form):
# LineEditControls
option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit'])
- option_fields += zip(['exclude_pattern'],[None],['line_edit'])
- option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
+ #***option_fields += zip(['exclude_pattern'],[None],['line_edit'])
+ #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
# SpinBoxControls
option_fields += zip(['thumb_width'],[1.00],['spin_box'])
- # Prefix rules TableWidget
+ # Exclusion rules
+ option_fields += zip(['exclusion_rules_tw','exclusion_rules_tw'],
+ [{'ordinal':0,
+ 'enabled':True,
+ 'name':'Catalogs',
+ 'field':'Tags',
+ 'pattern':'Catalog'},
+ {'ordinal':1,
+ 'enabled':False,
+ 'name':'New rule',
+ 'field':'',
+ 'pattern':''}],
+ ['table_widget','table_widget'])
+
+ # Prefix rules
option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'],
[{'ordinal':0,
'enabled':True,
@@ -123,13 +137,13 @@ class PluginWidget(QWidget,Ui_Form):
['exclude_source_field','header_note_source_field',
'merge_source_field']
LineEditControls (c_type: line_edit):
- ['exclude_genre','exclude_pattern','exclude_tags']
+ ['exclude_genre']
RadioButtonControls (c_type: radio_button):
['merge_before','merge_after']
SpinBoxControls (c_type: spin_box):
['thumb_width']
TableWidgetControls (c_type: table_widget):
- ['prefix_rules_tw']
+ ['exclusion_rules_tw','prefix_rules_tw']
'''
self.name = name
@@ -139,6 +153,7 @@ class PluginWidget(QWidget,Ui_Form):
# Update dialog fields from stored options
+ exclusion_rules = []
prefix_rules = []
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
@@ -159,16 +174,22 @@ class PluginWidget(QWidget,Ui_Form):
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
getattr(self, c_name).setValue(float(opt_value))
+ elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw':
+ if opt_value not in exclusion_rules:
+ exclusion_rules.append(opt_value)
elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw':
if opt_value not in prefix_rules:
prefix_rules.append(opt_value)
+ '''
+ ***
# Init self.exclude_source_field_name
self.exclude_source_field_name = ''
cs = unicode(self.exclude_source_field.currentText())
if cs > '':
exclude_source_spec = self.exclude_source_fields[cs]
self.exclude_source_field_name = exclude_source_spec['field']
+ '''
# Init self.merge_source_field_name
self.merge_source_field_name = ''
@@ -190,10 +211,13 @@ class PluginWidget(QWidget,Ui_Form):
# Hook changes to Description section
self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
+ # Initialize exclusion rules
+ self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl,
+ "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db)
+
# Initialize prefix rules
- self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw",
- prefix_rules, self.eligible_custom_fields,
- self.db)
+ self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl,
+ "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db)
def options(self):
# Save/return the current options
@@ -204,6 +228,8 @@ class PluginWidget(QWidget,Ui_Form):
opts_dict = {}
# Save values to gprefs
prefix_rules_processed = False
+ exclusion_rules_processed = False
+
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
if c_type in ['check_box', 'radio_button']:
@@ -215,7 +241,10 @@ class PluginWidget(QWidget,Ui_Form):
elif c_type in ['spin_box']:
opt_value = unicode(getattr(self, c_name).value())
elif c_type in ['table_widget']:
- opt_value = self.prefix_rules_table.get_data()
+ if c_name == 'prefix_rules_tw':
+ opt_value = self.prefix_rules_table.get_data()
+ if c_name == 'exclusion_rules_tw':
+ opt_value = self.exclusion_rules_table.get_data()
gprefs.set(self.name + '_' + c_name, opt_value)
@@ -246,19 +275,49 @@ class PluginWidget(QWidget,Ui_Form):
pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix'])
rule_set.append(pr)
-
opt_value = tuple(rule_set)
opts_dict['prefix_rules'] = opt_value
prefix_rules_processed = True
+ elif c_name == 'exclusion_rules_tw':
+ if exclusion_rules_processed:
+ continue
+ rule_set = []
+ for rule in opt_value:
+ # Test for empty name/field/pattern/prefix, continue
+ # If pattern = any or unspecified, convert to regex
+ if not rule['enabled']:
+ continue
+ elif not rule['field'] or not rule['pattern']:
+ continue
+ else:
+ if rule['field'] != 'Tags':
+ # Look up custom column name
+ #print(self.eligible_custom_fields[rule['field']]['field'])
+ rule['field'] = self.eligible_custom_fields[rule['field']]['field']
+ if rule['pattern'].startswith('any'):
+ rule['pattern'] = '.*'
+ elif rule['pattern'] == 'unspecified':
+ rule['pattern'] = 'None'
+
+ pr = (rule['name'],rule['field'],rule['pattern'])
+ rule_set.append(pr)
+
+ opt_value = tuple(rule_set)
+ opts_dict['exclusion_rules'] = opt_value
+ exclusion_rules_processed = True
+
else:
opts_dict[c_name] = opt_value
+ '''
+ ***
# Generate markers for hybrids
#opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name,
# self.read_pattern.text())
opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name,
self.exclude_pattern.text())
+ '''
# Generate specs for merge_comments, header_note_source_field
checked = ''
@@ -306,6 +365,8 @@ class PluginWidget(QWidget,Ui_Form):
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
+ '''
+ ***
# Blank field first
self.exclude_source_field.addItem('')
# Add the sorted eligible fields to the combo box
@@ -313,7 +374,7 @@ class PluginWidget(QWidget,Ui_Form):
self.exclude_source_field.addItem(cf)
self.exclude_source_fields = custom_fields
self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed)
-
+ '''
# Populate the 'Header note' combo box
custom_fields = {}
@@ -488,7 +549,7 @@ class NoWheelComboBox(QComboBox):
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
event.ignore()
-class PrefixRulesComboBox(NoWheelComboBox):
+class ComboBox(NoWheelComboBox):
# Caller is responsible for providing the list in the preferred order
def __init__(self, parent, items, selected_text,insert_blank=True):
NoWheelComboBox.__init__(self, parent)
@@ -511,8 +572,8 @@ class GenericRulesTable(QTableWidget):
placeholders for basic methods to be overriden
'''
- def __init__(self, parent_gb_hl, object_name, prefix_rules, eligible_custom_fields, db):
- self.prefix_rules = prefix_rules
+ def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db):
+ self.rules = rules
self.eligible_custom_fields = eligible_custom_fields
self.db = db
QTableWidget.__init__(self)
@@ -525,12 +586,11 @@ class GenericRulesTable(QTableWidget):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
self.setSizePolicy(sizePolicy)
- self.setMaximumSize(QSize(16777215, 118))
+ self.setMaximumSize(QSize(16777215, 114))
self.setColumnCount(0)
self.setRowCount(0)
self.layout.addWidget(self)
-
self._init_table_widget()
self._init_controls()
self._initialize()
@@ -686,10 +746,138 @@ class GenericRulesTable(QTableWidget):
'''
pass
+ def resize_name(self, scale):
+ current_width = self.columnWidth(1)
+ self.setColumnWidth(1, min(225,int(current_width * scale)))
+
+ def rule_name_edited(self):
+ current_row = self.currentRow()
+ self.cellWidget(current_row,1).home(False)
+ self.setFocus()
+ self.select_and_scroll_to_row(current_row)
+
+ def select_and_scroll_to_row(self, row):
+ self.selectRow(row)
+ self.scrollToItem(self.currentItem())
+
+class ExclusionRules(GenericRulesTable):
+
+ def _init_table_widget(self):
+ header_labels = ['','Name','Field','Value']
+ self.setColumnCount(len(header_labels))
+ self.setHorizontalHeaderLabels(header_labels)
+ self.setSortingEnabled(False)
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
+
+ def _initialize(self):
+ # Override max size (118) set in GenericRulesTable
+ self.setMaximumSize(QSize(16777215, 83))
+
+ self.populate()
+ self.resizeColumnsToContents()
+ self.resize_name(1.5)
+ self.horizontalHeader().setStretchLastSection(True)
+
+ def convert_row_to_data(self, row):
+ data = self.create_blank_row_data()
+ data['ordinal'] = row
+ data['enabled'] = self.item(row,0).checkState() == Qt.Checked
+ data['name'] = unicode(self.cellWidget(row,1).text()).strip()
+ data['field'] = unicode(self.cellWidget(row,2).currentText()).strip()
+ data['pattern'] = unicode(self.cellWidget(row,3).currentText()).strip()
+ return data
+
+ def create_blank_row_data(self):
+ data = {}
+ data['ordinal'] = -1
+ data['enabled'] = False
+ data['name'] = 'New rule'
+ data['field'] = ''
+ data['pattern'] = ''
+ return data
+
+ def get_data(self):
+ data_items = []
+ for row in range(self.rowCount()):
+ data = self.convert_row_to_data(row)
+ data_items.append(
+ {'ordinal':data['ordinal'],
+ 'enabled':data['enabled'],
+ 'name':data['name'],
+ 'field':data['field'],
+ 'pattern':data['pattern']})
+ return data_items
+
+ def populate(self):
+ # Format of rules list is different if default values vs retrieved JSON
+ # Hack to normalize list style
+ rules = self.rules
+ if rules and type(rules[0]) is list:
+ rules = rules[0]
+ self.setFocus()
+ rules = sorted(rules, key=lambda k: k['ordinal'])
+ for row, rule in enumerate(rules):
+ self.insertRow(row)
+ self.select_and_scroll_to_row(row)
+ self.populate_table_row(row, rule)
+ self.selectRow(0)
+
+ def populate_table_row(self, row, data):
+
+ def set_rule_name_in_row(row, col, name=''):
+ rule_name = QLineEdit(name)
+ rule_name.home(False)
+ rule_name.editingFinished.connect(self.rule_name_edited)
+ self.setCellWidget(row, col, rule_name)
+
+ def set_source_field_in_row(row, col, field=''):
+ source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field)
+ source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row))
+ self.setCellWidget(row, col, source_combo)
+ return source_combo
+
+ # Entry point
+ self.blockSignals(True)
+
+ # Column 0: Enabled
+ self.setItem(row, 0, CheckableTableWidgetItem(data['enabled']))
+
+ # Column 1: Rule name
+ set_rule_name_in_row(row, 1, name=data['name'])
+
+ # Column 2: Source field
+ source_combo = set_source_field_in_row(row, 2, field=data['field'])
+
+ # Column 3: Pattern
+ # The contents of the Pattern field is driven by the Source field
+ self.source_index_changed(source_combo, row, 3, pattern=data['pattern'])
+
+ self.blockSignals(False)
+
+ def source_index_changed(self, combo, row, col, pattern=''):
+ # Populate the Pattern field based upon the Source field
+ source_field = str(combo.currentText())
+ if source_field == '':
+ values = []
+ elif source_field == 'Tags':
+ values = sorted(self.db.all_tags(), key=sort_key)
+ else:
+ if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']:
+ values = self.db.all_custom(self.db.field_metadata.key_to_label(
+ self.eligible_custom_fields[source_field]['field']))
+ values = sorted(values, key=sort_key)
+ elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']:
+ values = ['True','False','unspecified']
+ elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']:
+ values = ['any value','unspecified']
+
+ values_combo = ComboBox(self, values, pattern)
+ self.setCellWidget(row, 3, values_combo)
+
class PrefixRules(GenericRulesTable):
def _init_table_widget(self):
- header_labels = ['','Name','Prefix','Source','Pattern']
+ header_labels = ['','Name','Prefix','Field','Value']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.setSortingEnabled(False)
@@ -873,8 +1061,8 @@ class PrefixRules(GenericRulesTable):
def populate(self):
# Format of rules list is different if default values vs retrieved JSON
# Hack to normalize list style
- rules = self.prefix_rules
- if type(rules[0]) is list:
+ rules = self.rules
+ if rules and type(rules[0]) is list:
rules = rules[0]
self.setFocus()
rules = sorted(rules, key=lambda k: k['ordinal'])
@@ -887,7 +1075,7 @@ class PrefixRules(GenericRulesTable):
def populate_table_row(self, row, data):
def set_prefix_field_in_row(row, col, field=''):
- prefix_combo = PrefixRulesComboBox(self, self.prefix_list, field)
+ prefix_combo = ComboBox(self, self.prefix_list, field)
self.setCellWidget(row, col, prefix_combo)
def set_rule_name_in_row(row, col, name=''):
@@ -897,7 +1085,7 @@ class PrefixRules(GenericRulesTable):
self.setCellWidget(row, col, rule_name)
def set_source_field_in_row(row, col, field=''):
- source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field)
+ source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field)
source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row))
self.setCellWidget(row, col, source_combo)
return source_combo
@@ -926,22 +1114,9 @@ class PrefixRules(GenericRulesTable):
self.blockSignals(False)
- def resize_name(self, scale):
- current_width = self.columnWidth(1)
- self.setColumnWidth(1, min(225,int(current_width * scale)))
-
- def rule_name_edited(self):
- current_row = self.currentRow()
- self.cellWidget(current_row,1).home(False)
- self.setFocus()
- self.select_and_scroll_to_row(current_row)
-
- def select_and_scroll_to_row(self, row):
- self.selectRow(row)
- self.scrollToItem(self.currentItem())
-
def source_index_changed(self, combo, row, col, pattern=''):
# Populate the Pattern field based upon the Source field
+ # row, col are the control that changed
source_field = str(combo.currentText())
if source_field == '':
@@ -960,6 +1135,6 @@ class PrefixRules(GenericRulesTable):
elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']:
values = ['any value','unspecified']
- values_combo = PrefixRulesComboBox(self, values, pattern)
+ values_combo = ComboBox(self, values, pattern)
self.setCellWidget(row, 4, values_combo)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index bf10cacd38..29ca39bf10 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -184,7 +184,7 @@ p, li { white-space: pre-wrap; }
-
+ 0
@@ -198,130 +198,14 @@ p, li { white-space: pre-wrap; }
- Books matching either pattern will not be included in generated catalog.
+ Matching books will not be included in generated catalog. Excluded books
-
-
- QFormLayout::FieldsStayAtSizeHint
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 175
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- Tags to &exclude
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- true
-
-
- exclude_tags
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- <p>Comma-separated list of tags to exclude.
-Default: ~,Catalog
-
-
-
-
-
-
-
-
-
-
-
- 175
- 0
-
-
-
-
- 200
- 16777215
-
-
-
- &Column/value
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- true
-
-
- exclude_source_field
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Column containing additional exclusion criteria
-
-
- QComboBox::AdjustToMinimumContentsLengthWithIcon
-
-
- 18
-
-
-
-
-
-
-
- 150
- 0
-
-
-
- Exclusion pattern
-
-
-
-
+
+
+
@@ -335,7 +219,7 @@ Default: ~,Catalog
- The first matching rule will be used to add a prefix to book listings in the generated catalog.
+ The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog.Prefix rules
@@ -369,9 +253,9 @@ Default: ~,Catalog
QFormLayout::FieldsStayAtSizeHint
-
+
-
+ 175
@@ -385,16 +269,13 @@ Default: ~,Catalog
- &Thumbnail width
+ &Thumb widthQt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
- true
-
- thumb_width
+ merge_source_field
@@ -406,6 +287,12 @@ Default: ~,Catalog
0
+
+
+ 137
+ 16777215
+
+ Size hint for Description cover thumbnails
@@ -426,38 +313,17 @@ Default: ~,Catalog
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 175
- 0
-
-
-
-
- 200
- 16777215
-
-
-
-
+
+
+ Qt::Vertical
+
+
+
+
- &Description note
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ &Extra noteheader_note_source_field
@@ -478,6 +344,12 @@ Default: ~,Catalog
0
+
+
+ 16777215
+ 16777215
+
+ Custom column source for note to include in Description header area
@@ -485,7 +357,7 @@ Default: ~,Catalog
-
+
diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py
index a7da345dc2..385a699c7b 100644
--- a/src/calibre/library/catalogs/epub_mobi.py
+++ b/src/calibre/library/catalogs/epub_mobi.py
@@ -48,29 +48,13 @@ class EPUB_MOBI(CatalogPlugin):
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--exclude-genre',
- default='\[.+\]',
+ default='\[.+\]|\+',
dest='exclude_genre',
action = None,
- help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n"
+ help=_("Regex describing tags to exclude as genres.\n"
+ "Default: '%default' excludes bracketed tags, e.g. '[Project Gutenberg]', and '+', the default tag for read books.\n"
"Applies to: ePub, MOBI output formats")),
-# Option('--exclude-book-marker',
-# default=':',
-# dest='exclude_book_marker',
-# action = None,
-# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n"
-# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n"
-# "Default: '%default'\n"
-# "Applies to ePub, MOBI output formats")),
-# Option('--exclude-tags',
-# default=('~,Catalog'),
-# dest='exclude_tags',
-# action = None,
-# help=_("Comma-separated list of tag words indicating book should be excluded from output. "
-# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. "
-# "Default:'%default'\n"
-# "Applies to: ePub, MOBI output formats")),
-
Option('--exclusion-rules',
default="(('Excluded tags','Tags','~,Catalog'),)",
dest='exclusion_rules',
diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py
index 6cc94c0e08..111ceda9fe 100644
--- a/src/calibre/library/catalogs/epub_mobi_builder.py
+++ b/src/calibre/library/catalogs/epub_mobi_builder.py
@@ -657,14 +657,36 @@ Author '{0}':
# Merge opts.exclude_tags with opts.search_text
# Updated to use exact match syntax
- empty_exclude_tags = False if len(self.opts.exclude_tags) else True
+
+ exclude_tags = []
+ for rule in self.opts.exclusion_rules:
+ if rule[1].lower() == 'tags':
+ exclude_tags.extend(rule[2].split(','))
+
+ # Remove dups
+ self.exclude_tags = exclude_tags = list(set(exclude_tags))
+
+ if self.opts.verbose and self.exclude_tags:
+ #self.opts.log.info(" excluding tag list %s" % exclude_tags)
+ search_terms = []
+ for tag in exclude_tags:
+ search_terms.append("tag:=%s" % tag)
+ search_phrase = "%s" % " or ".join(search_terms)
+ self.opts.search_text = search_phrase
+ data = self.plugin.search_sort_db(self.db, self.opts)
+ for record in data:
+ self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags))
+ # Reset the database
+ self.opts.search_text = ''
+ data = self.plugin.search_sort_db(self.db, self.opts)
+
search_phrase = ''
- if not empty_exclude_tags:
- exclude_tags = self.opts.exclude_tags.split(',')
+ if exclude_tags:
search_terms = []
for tag in exclude_tags:
search_terms.append("tag:=%s" % tag)
search_phrase = "not (%s)" % " or ".join(search_terms)
+
# If a list of ids are provided, don't use search_text
if self.opts.ids:
self.opts.search_text = search_phrase
@@ -1672,14 +1694,13 @@ Author '{0}':
self.opts.sort_by = 'series'
- # Merge opts.exclude_tags with opts.search_text
+ # Merge self.exclude_tags with opts.search_text
# Updated to use exact match syntax
- empty_exclude_tags = False if len(self.opts.exclude_tags) else True
+
search_phrase = 'series:true '
- if not empty_exclude_tags:
- exclude_tags = self.opts.exclude_tags.split(',')
+ if self.exclude_tags:
search_terms = []
- for tag in exclude_tags:
+ for tag in self.exclude_tags:
search_terms.append("tag:=%s" % tag)
search_phrase += "not (%s)" % " or ".join(search_terms)
@@ -3120,7 +3141,7 @@ Author '{0}':
Evaluate conditions for including prefixes in various listings
'''
def log_prefix_rule_match_info(rule, record):
- self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" %
+ self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" %
(rule['prefix'],record['title'],
record['authors'][0], rule['name'],
rule['field'],rule['pattern']))
@@ -3816,9 +3837,14 @@ Author '{0}':
return friendly_tag
def getMarkerTags(self):
- ''' Return a list of special marker tags to be excluded from genre list '''
+ '''
+ Return a list of special marker tags to be excluded from genre list
+ exclusion_rules = ('name','Tags|#column','[]|pattern')
+ '''
markerTags = []
- markerTags.extend(self.opts.exclude_tags.split(','))
+ for rule in self.opts.exclusion_rules:
+ if rule[1].lower() == 'tags':
+ markerTags.extend(rule[2].split(','))
return markerTags
def letter_or_symbol(self,char):
@@ -3996,21 +4022,40 @@ Author '{0}':
'''
Remove excluded entries
'''
- field, pat = self.opts.exclude_book_marker.split(':')
- if pat == '':
- return data_set
filtered_data_set = []
- for record in data_set:
- field_contents = self.__db.get_field(record['id'],
- field,
- index_is_id=True)
- if field_contents:
- if re.search(pat, unicode(field_contents),
- re.IGNORECASE) is not None:
- continue
- filtered_data_set.append(record)
+ exclusion_pairs = []
+ exclusion_set = []
+ for rule in self.opts.exclusion_rules:
+ if rule[1].startswith('#') and rule[2] != '':
+ field = rule[1]
+ pat = rule[2]
+ exclusion_pairs.append((field,pat))
+ else:
+ continue
- return filtered_data_set
+ if exclusion_pairs:
+ for record in data_set:
+ for exclusion_pair in exclusion_pairs:
+ field,pat = exclusion_pair
+ field_contents = self.__db.get_field(record['id'],
+ field,
+ index_is_id=True)
+ if field_contents:
+ if re.search(pat, unicode(field_contents),
+ re.IGNORECASE) is not None:
+ if self.opts.verbose:
+ self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat))
+ exclusion_set.append(record)
+ if record in filtered_data_set:
+ filtered_data_set.remove(record)
+ break
+ else:
+ if (record not in filtered_data_set and
+ record not in exclusion_set):
+ filtered_data_set.append(record)
+ return filtered_data_set
+ else:
+ return data_set
def processSpecialTags(self, tags, this_title, opts):
From 1f39af8010a304e39356678c5756194a6d82435c Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 6 Aug 2012 18:52:00 +0200
Subject: [PATCH 17/27] Add collections to the smartdevice driver
---
.../devices/smart_device_app/driver.py | 42 +++++++++++++++----
1 file changed, 33 insertions(+), 9 deletions(-)
diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index b88c97a279..ee16e23a25 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -17,7 +17,7 @@ from calibre.constants import numeric_version, DEBUG
from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError,
InitialConnectionError)
from calibre.devices.interface import DevicePlugin
-from calibre.devices.usbms.books import Book, BookList
+from calibre.devices.usbms.books import Book, CollectionsBookList
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.usbms.driver import USBMS
from calibre.ebooks import BOOK_EXTENSIONS
@@ -107,8 +107,18 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
}
reverse_opcodes = dict([(v, k) for k,v in opcodes.iteritems()])
+ ALL_BY_TITLE = _('All by title')
+ ALL_BY_AUTHOR = _('All by author')
EXTRA_CUSTOMIZATION_MESSAGE = [
+ _('Comma separated list of metadata fields '
+ 'to turn into collections on the device. Possibilities include: ')+\
+ 'series, tags, authors' +\
+ _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add '
+ 'these values to the list to enable them. The collections will be '
+ 'given the name provided after the ":" character.')%dict(
+ abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR),
+ '',
_('Enable connections at startup') + ':::
' +
_('Check this box to allow connections when calibre starts') + '
',
'',
@@ -124,6 +134,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
_('Check this box if requested when reporting problems') + '
',
]
EXTRA_CUSTOMIZATION_DEFAULT = [
+ 'tags, series',
+ '',
False,
'',
'',
@@ -131,11 +143,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
False, '9090',
False,
]
- OPT_AUTOSTART = 0
- OPT_PASSWORD = 2
- OPT_USE_PORT = 4
- OPT_PORT_NUMBER = 5
- OPT_EXTRA_DEBUG = 6
+ OPT_COLLECTIONS = 0
+ OPT_AUTOSTART = 2
+ OPT_PASSWORD = 4
+ OPT_USE_PORT = 6
+ OPT_PORT_NUMBER = 7
+ OPT_EXTRA_DEBUG = 8
def __init__(self, path):
self.sync_lock = threading.RLock()
@@ -659,9 +672,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
def books(self, oncard=None, end_session=True):
self._debug(oncard)
if oncard is not None:
- return BookList(None, None, None)
+ return CollectionsBookList(None, None, None)
opcode, result = self._call_client('GET_BOOK_COUNT', {})
- bl = BookList(None, self.PREFIX, self.settings)
+ bl = CollectionsBookList(None, self.PREFIX, self.settings)
if opcode == 'OK':
count = result['count']
for i in range(0, count):
@@ -681,10 +694,21 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock')
def sync_booklists(self, booklists, end_session=True):
self._debug()
+ collections = [x.strip() for x in
+ self.settings().extra_customization[self.OPT_COLLECTIONS].split(',')]
+ collections = booklists[0].get_collections(collections)
+ coldict = {}
+ for k,v in collections.iteritems():
+ lpaths = []
+ for book in v:
+ lpaths.append(book.lpath)
+ coldict[k] = lpaths
+ self._debug(coldict)
# If we ever do device_db plugboards, this is where it will go. We will
# probably need to send two booklists, one with calibre's data that is
# given back by "books", and one that has been plugboarded.
- self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]) } )
+ self._call_client('SEND_BOOKLISTS', { 'count': len(booklists[0]),
+ 'collections': coldict} )
for i,book in enumerate(booklists[0]):
if not self._metadata_already_on_device(book):
self._set_known_metadata(book)
From e40bd0164dbf84ab0ceb3f6211591b08cd89687a Mon Sep 17 00:00:00 2001
From: GRiker
Date: Mon, 6 Aug 2012 10:52:07 -0600
Subject: [PATCH 18/27] Removed table selection when lost focus, added reset
button to exclude_genres
---
src/calibre/gui2/catalog/catalog_epub_mobi.py | 124 +++++++-----------
src/calibre/gui2/catalog/catalog_epub_mobi.ui | 13 +-
.../library/catalogs/epub_mobi_builder.py | 18 +--
3 files changed, 69 insertions(+), 86 deletions(-)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index 8a2035c76f..57a23211e3 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -18,7 +18,7 @@ from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QDialog,
QDialogButtonBox, QDoubleSpinBox,
QHBoxLayout, QIcon, QLabel, QLineEdit,
QPlainTextEdit, QRadioButton, QSize, QSizePolicy,
- QTableWidget, QTableWidgetItem,
+ QTableWidget, QTableWidgetItem, QTimer,
QToolButton, QVBoxLayout, QWidget)
class PluginWidget(QWidget,Ui_Form):
@@ -72,8 +72,6 @@ class PluginWidget(QWidget,Ui_Form):
# LineEditControls
option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit'])
- #***option_fields += zip(['exclude_pattern'],[None],['line_edit'])
- #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
# SpinBoxControls
option_fields += zip(['thumb_width'],[1.00],['spin_box'])
@@ -84,12 +82,7 @@ class PluginWidget(QWidget,Ui_Form):
'enabled':True,
'name':'Catalogs',
'field':'Tags',
- 'pattern':'Catalog'},
- {'ordinal':1,
- 'enabled':False,
- 'name':'New rule',
- 'field':'',
- 'pattern':''}],
+ 'pattern':'Catalog'},],
['table_widget','table_widget'])
# Prefix rules
@@ -105,13 +98,7 @@ class PluginWidget(QWidget,Ui_Form):
'name':'Wishlist item',
'field':'Tags',
'pattern':'Wishlist',
- 'prefix':u'\u00d7'},
- {'ordinal':2,
- 'enabled':False,
- 'name':'New rule',
- 'field':'',
- 'pattern':'',
- 'prefix':''}],
+ 'prefix':u'\u00d7'},],
['table_widget','table_widget','table_widget'])
self.OPTION_FIELDS = option_fields
@@ -181,15 +168,9 @@ class PluginWidget(QWidget,Ui_Form):
if opt_value not in prefix_rules:
prefix_rules.append(opt_value)
- '''
- ***
- # Init self.exclude_source_field_name
- self.exclude_source_field_name = ''
- cs = unicode(self.exclude_source_field.currentText())
- if cs > '':
- exclude_source_spec = self.exclude_source_fields[cs]
- self.exclude_source_field_name = exclude_source_spec['field']
- '''
+ # Add icon to the reset button
+ self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png')))
+ self.reset_exclude_genres_tb.clicked.connect(self.reset_exclude_genres)
# Init self.merge_source_field_name
self.merge_source_field_name = ''
@@ -205,12 +186,6 @@ class PluginWidget(QWidget,Ui_Form):
header_note_source_spec = self.header_note_source_fields[cs]
self.header_note_source_field_name = header_note_source_spec['field']
- # Hook changes to thumb_width
- self.thumb_width.valueChanged.connect(self.thumb_width_changed)
-
- # Hook changes to Description section
- self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
-
# Initialize exclusion rules
self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl,
"exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db)
@@ -219,6 +194,13 @@ class PluginWidget(QWidget,Ui_Form):
self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl,
"prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db)
+ # Hook changes to thumb_width
+ self.thumb_width.valueChanged.connect(self.thumb_width_changed)
+
+ # Hook changes to Description section
+ self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
+
+
def options(self):
# Save/return the current options
# exclude_genre stores literally
@@ -310,15 +292,6 @@ class PluginWidget(QWidget,Ui_Form):
else:
opts_dict[c_name] = opt_value
- '''
- ***
- # Generate markers for hybrids
- #opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name,
- # self.read_pattern.text())
- opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name,
- self.exclude_pattern.text())
- '''
-
# Generate specs for merge_comments, header_note_source_field
checked = ''
if self.merge_before.isChecked():
@@ -365,17 +338,6 @@ class PluginWidget(QWidget,Ui_Form):
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
- '''
- ***
- # Blank field first
- self.exclude_source_field.addItem('')
- # Add the sorted eligible fields to the combo box
- for cf in sorted(custom_fields, key=sort_key):
- self.exclude_source_field.addItem(cf)
- self.exclude_source_fields = custom_fields
- self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed)
- '''
-
# Populate the 'Header note' combo box
custom_fields = {}
for custom_field in self.all_custom_fields:
@@ -508,6 +470,12 @@ class PluginWidget(QWidget,Ui_Form):
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
+ def reset_exclude_genres(self):
+ for default in self.OPTION_FIELDS:
+ if default[0] == 'exclude_genre':
+ self.exclude_genre.setText(default[1])
+ break
+
def thumb_width_changed(self,new_value):
'''
Process changes in the thumb_width spin box
@@ -581,19 +549,20 @@ class GenericRulesTable(QTableWidget):
self.layout = parent_gb_hl
# Add ourselves to the layout
- sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
+ #print("verticalHeader: %s" % dir(self.verticalHeader()))
+ sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
+ #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
self.setSizePolicy(sizePolicy)
- self.setMaximumSize(QSize(16777215, 114))
+ self.setMaximumSize(QSize(16777215, 113))
+
self.setColumnCount(0)
self.setRowCount(0)
+
self.layout.addWidget(self)
- self._init_table_widget()
self._init_controls()
- self._initialize()
def _init_controls(self):
# Add the control set
@@ -628,18 +597,6 @@ class GenericRulesTable(QTableWidget):
self.layout.addLayout(vbl)
- def _init_table_widget(self):
- '''
- Override this in the specific instance
- '''
- pass
-
- def _initialize(self):
- '''
- Override this in the specific instance
- '''
- pass
-
def add_row(self):
self.setFocus()
row = self.currentRow() + 1
@@ -683,6 +640,10 @@ class GenericRulesTable(QTableWidget):
def get_data(self):
pass
+ def focusOutEvent(self,e):
+ # Override of QTableWidget method
+ self.clearSelection()
+
def move_row_down(self):
self.setFocus()
rows = self.selectionModel().selectedRows()
@@ -760,8 +721,21 @@ class GenericRulesTable(QTableWidget):
self.selectRow(row)
self.scrollToItem(self.currentItem())
+ def tweak_height(self, height=4):
+ for i in range(min(3,self.rowCount())):
+ height += self.rowHeight(i)
+ height += self.verticalHeader().sizeHint().height()
+ print("computed table height for %d rows: %d" % (self.rowCount(),height, ))
+ self.setMinimumSize(QSize(16777215, height))
+ self.setMaximumSize(QSize(16777215, height))
+
class ExclusionRules(GenericRulesTable):
+ def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db):
+ super(ExclusionRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db)
+ self._init_table_widget()
+ self._initialize()
+
def _init_table_widget(self):
header_labels = ['','Name','Field','Value']
self.setColumnCount(len(header_labels))
@@ -770,13 +744,11 @@ class ExclusionRules(GenericRulesTable):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def _initialize(self):
- # Override max size (118) set in GenericRulesTable
- self.setMaximumSize(QSize(16777215, 83))
-
self.populate()
self.resizeColumnsToContents()
self.resize_name(1.5)
self.horizontalHeader().setStretchLastSection(True)
+ self.clearSelection()
def convert_row_to_data(self, row):
data = self.create_blank_row_data()
@@ -790,7 +762,7 @@ class ExclusionRules(GenericRulesTable):
def create_blank_row_data(self):
data = {}
data['ordinal'] = -1
- data['enabled'] = False
+ data['enabled'] = True
data['name'] = 'New rule'
data['field'] = ''
data['pattern'] = ''
@@ -876,6 +848,11 @@ class ExclusionRules(GenericRulesTable):
class PrefixRules(GenericRulesTable):
+ def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db):
+ super(PrefixRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db)
+ self._init_table_widget()
+ self._initialize()
+
def _init_table_widget(self):
header_labels = ['','Name','Prefix','Field','Value']
self.setColumnCount(len(header_labels))
@@ -889,6 +866,7 @@ class PrefixRules(GenericRulesTable):
self.resizeColumnsToContents()
self.resize_name(1.5)
self.horizontalHeader().setStretchLastSection(True)
+ self.clearSelection()
def convert_row_to_data(self, row):
data = self.create_blank_row_data()
@@ -903,7 +881,7 @@ class PrefixRules(GenericRulesTable):
def create_blank_row_data(self):
data = {}
data['ordinal'] = -1
- data['enabled'] = False
+ data['enabled'] = True
data['name'] = 'New rule'
data['field'] = ''
data['pattern'] = ''
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index 29ca39bf10..784806c15e 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -111,7 +111,8 @@
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html>
+<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p>
+<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html>
Excluded genres
@@ -178,6 +179,16 @@ p, li { white-space: pre-wrap; }
+
+
+
+ Reset to default
+
+
+ ...
+
+
+
diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py
index 111ceda9fe..63fe02d5f1 100644
--- a/src/calibre/library/catalogs/epub_mobi_builder.py
+++ b/src/calibre/library/catalogs/epub_mobi_builder.py
@@ -666,19 +666,13 @@ Author '{0}':
# Remove dups
self.exclude_tags = exclude_tags = list(set(exclude_tags))
+ # Report tag exclusions
if self.opts.verbose and self.exclude_tags:
- #self.opts.log.info(" excluding tag list %s" % exclude_tags)
- search_terms = []
- for tag in exclude_tags:
- search_terms.append("tag:=%s" % tag)
- search_phrase = "%s" % " or ".join(search_terms)
- self.opts.search_text = search_phrase
- data = self.plugin.search_sort_db(self.db, self.opts)
+ data = self.db.get_data_as_dict(ids=self.opts.ids)
for record in data:
- self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags))
- # Reset the database
- self.opts.search_text = ''
- data = self.plugin.search_sort_db(self.db, self.opts)
+ matched = list(set(record['tags']) & set(exclude_tags))
+ if matched :
+ self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched))
search_phrase = ''
if exclude_tags:
@@ -3141,7 +3135,7 @@ Author '{0}':
Evaluate conditions for including prefixes in various listings
'''
def log_prefix_rule_match_info(rule, record):
- self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" %
+ self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" %
(rule['prefix'],record['title'],
record['authors'][0], rule['name'],
rule['field'],rule['pattern']))
From aa62a13cf0ec321d822f2b4944cfce61f65540cd Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 6 Aug 2012 18:55:28 +0200
Subject: [PATCH 19/27] ...
---
src/calibre/devices/smart_device_app/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index ee16e23a25..82ff2e7920 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -703,7 +703,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
for book in v:
lpaths.append(book.lpath)
coldict[k] = lpaths
- self._debug(coldict)
+
# If we ever do device_db plugboards, this is where it will go. We will
# probably need to send two booklists, one with calibre's data that is
# given back by "books", and one that has been plugboarded.
From a8b5d497fc622959a13a4a427017b9f43b52fb1b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 6 Aug 2012 19:01:15 +0200
Subject: [PATCH 20/27] ...
---
src/calibre/devices/smart_device_app/driver.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index 82ff2e7920..5fb9761d32 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -693,16 +693,17 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock')
def sync_booklists(self, booklists, end_session=True):
- self._debug()
- collections = [x.strip() for x in
+ colattrs = [x.strip() for x in
self.settings().extra_customization[self.OPT_COLLECTIONS].split(',')]
- collections = booklists[0].get_collections(collections)
+ self._debug('collection attributes', colattrs)
coldict = {}
- for k,v in collections.iteritems():
- lpaths = []
- for book in v:
- lpaths.append(book.lpath)
- coldict[k] = lpaths
+ if colattrs:
+ collections = booklists[0].get_collections(colattrs)
+ for k,v in collections.iteritems():
+ lpaths = []
+ for book in v:
+ lpaths.append(book.lpath)
+ coldict[k] = lpaths
# If we ever do device_db plugboards, this is where it will go. We will
# probably need to send two booklists, one with calibre's data that is
From 1f2e84181f9d77efaed4152a77b89b188909621d Mon Sep 17 00:00:00 2001
From: GRiker
Date: Mon, 6 Aug 2012 17:34:47 -0600
Subject: [PATCH 21/27] Improved focus handling for tables. Removed obsolete
characters in profiles.py. Revised tool tips.
---
src/calibre/customize/profiles.py | 8 -
src/calibre/gui2/catalog/catalog_epub_mobi.py | 205 ++++++------------
src/calibre/gui2/catalog/catalog_epub_mobi.ui | 36 +--
.../library/catalogs/epub_mobi_builder.py | 7 +-
4 files changed, 84 insertions(+), 172 deletions(-)
diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py
index 84db12e161..78b61d4345 100644
--- a/src/calibre/customize/profiles.py
+++ b/src/calibre/customize/profiles.py
@@ -251,10 +251,8 @@ class OutputProfile(Plugin):
periodical_date_in_title = True
#: Characters used in jackets and catalogs
- missing_char = u'x'
ratings_char = u'*'
empty_ratings_char = u' '
- read_char = u'+'
#: Unsupported unicode characters to be replaced during preprocessing
unsupported_unicode_chars = []
@@ -292,10 +290,8 @@ class iPadOutput(OutputProfile):
}
]
- missing_char = u'\u2715\u200a' # stylized 'x' plus hair space
ratings_char = u'\u2605' # filled star
empty_ratings_char = u'\u2606' # hollow star
- read_char = u'\u2713' # check mark
touchscreen = True
# touchscreen_news_css {{{
@@ -626,10 +622,8 @@ class KindleOutput(OutputProfile):
supports_mobi_indexing = True
periodical_date_in_title = False
- missing_char = u'x\u2009'
empty_ratings_char = u'\u2606'
ratings_char = u'\u2605'
- read_char = u'\u2713'
mobi_ems_per_blockquote = 2.0
@@ -651,10 +645,8 @@ class KindleDXOutput(OutputProfile):
#comic_screen_size = (741, 1022)
supports_mobi_indexing = True
periodical_date_in_title = False
- missing_char = u'x\u2009'
empty_ratings_char = u'\u2606'
ratings_char = u'\u2605'
- read_char = u'\u2713'
mobi_ems_per_blockquote = 2.0
@classmethod
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index 57a23211e3..485f84a642 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -6,6 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+from copy import copy
from functools import partial
from calibre.ebooks.conversion.config import load_defaults
@@ -138,7 +139,6 @@ class PluginWidget(QWidget,Ui_Form):
self.fetchEligibleCustomFields()
self.populate_combo_boxes()
-
# Update dialog fields from stored options
exclusion_rules = []
prefix_rules = []
@@ -161,12 +161,13 @@ class PluginWidget(QWidget,Ui_Form):
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
getattr(self, c_name).setValue(float(opt_value))
- elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw':
- if opt_value not in exclusion_rules:
- exclusion_rules.append(opt_value)
- elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw':
- if opt_value not in prefix_rules:
- prefix_rules.append(opt_value)
+ if c_type == 'table_widget':
+ if c_name == 'exclusion_rules_tw':
+ if opt_value not in exclusion_rules:
+ exclusion_rules.append(opt_value)
+ if c_name == 'prefix_rules_tw':
+ if opt_value not in prefix_rules:
+ prefix_rules.append(opt_value)
# Add icon to the reset button
self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png')))
@@ -187,20 +188,13 @@ class PluginWidget(QWidget,Ui_Form):
self.header_note_source_field_name = header_note_source_spec['field']
# Initialize exclusion rules
- self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl,
+ self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb,
"exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db)
# Initialize prefix rules
- self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl,
+ self.prefix_rules_table = PrefixRules(self.prefix_rules_gb,
"prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db)
- # Hook changes to thumb_width
- self.thumb_width.valueChanged.connect(self.thumb_width_changed)
-
- # Hook changes to Description section
- self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
-
-
def options(self):
# Save/return the current options
# exclude_genre stores literally
@@ -214,6 +208,11 @@ class PluginWidget(QWidget,Ui_Form):
for opt in self.OPTION_FIELDS:
c_name, c_def, c_type = opt
+ if c_name == 'exclusion_rules_tw' and exclusion_rules_processed:
+ continue
+ if c_name == 'prefix_rules_tw' and prefix_rules_processed:
+ continue
+
if c_type in ['check_box', 'radio_button']:
opt_value = getattr(self, c_name).isChecked()
elif c_type in ['combo_box']:
@@ -225,22 +224,21 @@ class PluginWidget(QWidget,Ui_Form):
elif c_type in ['table_widget']:
if c_name == 'prefix_rules_tw':
opt_value = self.prefix_rules_table.get_data()
+ prefix_rules_processed = True
if c_name == 'exclusion_rules_tw':
opt_value = self.exclusion_rules_table.get_data()
+ exclusion_rules_processed = True
+ # Store UI values to gui.json in config dir
gprefs.set(self.name + '_' + c_name, opt_value)
# Construct opts object for catalog builder
- if c_name == 'exclude_tags':
- # store as list
- opts_dict[c_name] = opt_value.split(',')
- elif c_name == 'prefix_rules_tw':
- if prefix_rules_processed:
- continue
+ if c_name == 'prefix_rules_tw':
rule_set = []
- for rule in opt_value:
+ for stored_rule in opt_value:
# Test for empty name/field/pattern/prefix, continue
# If pattern = any or unspecified, convert to regex
+ rule = copy(stored_rule)
if not rule['enabled']:
continue
elif not rule['field'] or not rule['pattern'] or not rule['prefix']:
@@ -250,24 +248,22 @@ class PluginWidget(QWidget,Ui_Form):
# Look up custom column name
#print(self.eligible_custom_fields[rule['field']]['field'])
rule['field'] = self.eligible_custom_fields[rule['field']]['field']
- if rule['pattern'].startswith('any'):
- rule['pattern'] = '.*'
- elif rule['pattern'] == 'unspecified':
- rule['pattern'] = 'None'
+ if rule['pattern'].startswith('any'):
+ rule['pattern'] = '.*'
+ elif rule['pattern'] == 'unspecified':
+ rule['pattern'] = 'None'
- pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix'])
- rule_set.append(pr)
+ pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix'])
+ rule_set.append(pr)
opt_value = tuple(rule_set)
opts_dict['prefix_rules'] = opt_value
- prefix_rules_processed = True
elif c_name == 'exclusion_rules_tw':
- if exclusion_rules_processed:
- continue
rule_set = []
- for rule in opt_value:
+ for stored_rule in opt_value:
# Test for empty name/field/pattern/prefix, continue
# If pattern = any or unspecified, convert to regex
+ rule = copy(stored_rule)
if not rule['enabled']:
continue
elif not rule['field'] or not rule['pattern']:
@@ -277,17 +273,15 @@ class PluginWidget(QWidget,Ui_Form):
# Look up custom column name
#print(self.eligible_custom_fields[rule['field']]['field'])
rule['field'] = self.eligible_custom_fields[rule['field']]['field']
- if rule['pattern'].startswith('any'):
- rule['pattern'] = '.*'
- elif rule['pattern'] == 'unspecified':
- rule['pattern'] = 'None'
-
- pr = (rule['name'],rule['field'],rule['pattern'])
- rule_set.append(pr)
+ if rule['pattern'].startswith('any'):
+ rule['pattern'] = '.*'
+ elif rule['pattern'] == 'unspecified':
+ rule['pattern'] = 'None'
+ pr = (rule['name'],rule['field'],rule['pattern'])
+ rule_set.append(pr)
opt_value = tuple(rule_set)
opts_dict['exclusion_rules'] = opt_value
- exclusion_rules_processed = True
else:
opts_dict[c_name] = opt_value
@@ -372,74 +366,6 @@ class PluginWidget(QWidget,Ui_Form):
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
- def read_source_field_changed(self,new_index):
- '''
- Process changes in the read_source_field combo box
- Currently using QLineEdit for all field types
- Possible to modify to switch QWidget type
- '''
- new_source = unicode(self.read_source_field.currentText())
- read_source_spec = self.read_source_fields[new_source]
- self.read_source_field_name = read_source_spec['field']
-
- # Change pattern input widget to match the source field datatype
- if read_source_spec['datatype'] in ['bool','composite','datetime','text']:
- if not isinstance(self.read_pattern, QLineEdit):
- self.read_spec_hl.removeWidget(self.read_pattern)
- dw = QLineEdit(self)
- dw.setObjectName('read_pattern')
- dw.setToolTip('Pattern for read book')
- self.read_pattern = dw
- self.read_spec_hl.addWidget(dw)
-
- def exclude_source_field_changed(self,new_index):
- '''
- Process changes in the exclude_source_field combo box
- Currently using QLineEdit for all field types
- Possible to modify to switch QWidget type
- '''
- new_source = str(self.exclude_source_field.currentText())
- self.exclude_source_field_name = new_source
- if new_source > '':
- exclude_source_spec = self.exclude_source_fields[unicode(new_source)]
- self.exclude_source_field_name = exclude_source_spec['field']
- self.exclude_pattern.setEnabled(True)
-
- # Change pattern input widget to match the source field datatype
- if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']:
- if not isinstance(self.exclude_pattern, QLineEdit):
- self.exclude_spec_hl.removeWidget(self.exclude_pattern)
- dw = QLineEdit(self)
- dw.setObjectName('exclude_pattern')
- dw.setToolTip('Exclusion pattern')
- self.exclude_pattern = dw
- self.exclude_spec_hl.addWidget(dw)
- else:
- self.exclude_pattern.setEnabled(False)
-
- def generate_descriptions_changed(self,new_state):
- '''
- Process changes to Descriptions section
- 0: unchecked
- 2: checked
- '''
-
- return
- '''
- if new_state == 0:
- # unchecked
- self.merge_source_field.setEnabled(False)
- self.merge_before.setEnabled(False)
- self.merge_after.setEnabled(False)
- self.include_hr.setEnabled(False)
- elif new_state == 2:
- # checked
- self.merge_source_field.setEnabled(True)
- self.merge_before.setEnabled(True)
- self.merge_after.setEnabled(True)
- self.include_hr.setEnabled(True)
- '''
-
def header_note_source_field_changed(self,new_index):
'''
Process changes in the header_note_source_field combo box
@@ -476,12 +402,6 @@ class PluginWidget(QWidget,Ui_Form):
self.exclude_genre.setText(default[1])
break
- def thumb_width_changed(self,new_value):
- '''
- Process changes in the thumb_width spin box
- '''
- pass
-
class CheckableTableWidgetItem(QTableWidgetItem):
'''
@@ -540,13 +460,14 @@ class GenericRulesTable(QTableWidget):
placeholders for basic methods to be overriden
'''
- def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db):
+ def __init__(self, parent_gb, object_name, rules, eligible_custom_fields, db):
self.rules = rules
self.eligible_custom_fields = eligible_custom_fields
self.db = db
QTableWidget.__init__(self)
self.setObjectName(object_name)
- self.layout = parent_gb_hl
+ self.layout = QHBoxLayout()
+ parent_gb.setLayout(self.layout)
# Add ourselves to the layout
#print("verticalHeader: %s" % dir(self.verticalHeader()))
@@ -559,9 +480,11 @@ class GenericRulesTable(QTableWidget):
self.setColumnCount(0)
self.setRowCount(0)
-
self.layout.addWidget(self)
+ self.last_row_selected = self.currentRow()
+ self.last_rows_selected = self.selectionModel().selectedRows()
+
self._init_controls()
def _init_controls(self):
@@ -599,12 +522,12 @@ class GenericRulesTable(QTableWidget):
def add_row(self):
self.setFocus()
- row = self.currentRow() + 1
+ row = self.last_row_selected + 1
self.insertRow(row)
self.populate_table_row(row, self.create_blank_row_data())
self.select_and_scroll_to_row(row)
self.resizeColumnsToContents()
- # Just in case table was empty
+ # In case table was empty
self.horizontalHeader().setStretchLastSection(True)
def convert_row_to_data(self):
@@ -621,12 +544,16 @@ class GenericRulesTable(QTableWidget):
def delete_row(self):
self.setFocus()
- rows = self.selectionModel().selectedRows()
+ rows = self.last_rows_selected
if len(rows) == 0:
return
- message = '
Are you sure you want to delete this rule?'
+
+ first = rows[0].row() + 1
+ last = rows[-1].row() + 1
+
+ message = '
Are you sure you want to delete rule %d?' % first
if len(rows) > 1:
- message = '
Are you sure you want to delete the %d selected rules?'%len(rows)
+ message = '
Are you sure you want to delete rules %d-%d?' % (first, last)
if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False):
return
first_sel_row = self.currentRow()
@@ -637,16 +564,18 @@ class GenericRulesTable(QTableWidget):
elif self.rowCount() > 0:
self.select_and_scroll_to_row(first_sel_row - 1)
+ def focusOutEvent(self,e):
+ # Override of QTableWidget method - clear selection when table loses focus
+ self.last_row_selected = self.currentRow()
+ self.last_rows_selected = self.selectionModel().selectedRows()
+ self.clearSelection()
+
def get_data(self):
pass
- def focusOutEvent(self,e):
- # Override of QTableWidget method
- self.clearSelection()
-
def move_row_down(self):
self.setFocus()
- rows = self.selectionModel().selectedRows()
+ rows = self.last_rows_selected
if len(rows) == 0:
return
last_sel_row = rows[-1].row()
@@ -673,11 +602,11 @@ class GenericRulesTable(QTableWidget):
scroll_to_row = last_sel_row + 1
if scroll_to_row < self.rowCount() - 1:
scroll_to_row = scroll_to_row + 1
- self.scrollToItem(self.item(scroll_to_row, 0))
+ self.select_and_scroll_to_row(scroll_to_row)
def move_row_up(self):
self.setFocus()
- rows = self.selectionModel().selectedRows()
+ rows = self.last_rows_selected
if len(rows) == 0:
return
first_sel_row = rows[0].row()
@@ -699,7 +628,7 @@ class GenericRulesTable(QTableWidget):
scroll_to_row = first_sel_row - 1
if scroll_to_row > 0:
scroll_to_row = scroll_to_row - 1
- self.scrollToItem(self.item(scroll_to_row, 0))
+ self.select_and_scroll_to_row(scroll_to_row)
def populate_table_row(self):
'''
@@ -744,7 +673,7 @@ class ExclusionRules(GenericRulesTable):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def _initialize(self):
- self.populate()
+ self.populate_table()
self.resizeColumnsToContents()
self.resize_name(1.5)
self.horizontalHeader().setStretchLastSection(True)
@@ -780,7 +709,7 @@ class ExclusionRules(GenericRulesTable):
'pattern':data['pattern']})
return data_items
- def populate(self):
+ def populate_table(self):
# Format of rules list is different if default values vs retrieved JSON
# Hack to normalize list style
rules = self.rules
@@ -842,6 +771,8 @@ class ExclusionRules(GenericRulesTable):
values = ['True','False','unspecified']
elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']:
values = ['any value','unspecified']
+ elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']:
+ values = ['any date','unspecified']
values_combo = ComboBox(self, values, pattern)
self.setCellWidget(row, 3, values_combo)
@@ -862,7 +793,7 @@ class PrefixRules(GenericRulesTable):
def _initialize(self):
self.generate_prefix_list()
- self.populate()
+ self.populate_table()
self.resizeColumnsToContents()
self.resize_name(1.5)
self.horizontalHeader().setStretchLastSection(True)
@@ -1036,7 +967,7 @@ class PrefixRules(GenericRulesTable):
'prefix':data['prefix']})
return data_items
- def populate(self):
+ def populate_table(self):
# Format of rules list is different if default values vs retrieved JSON
# Hack to normalize list style
rules = self.rules
@@ -1108,10 +1039,10 @@ class PrefixRules(GenericRulesTable):
values = sorted(values, key=sort_key)
elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']:
values = ['True','False','unspecified']
- elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']:
- values = ['any date','unspecified']
elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']:
values = ['any value','unspecified']
+ elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']:
+ values = ['any date','unspecified']
values_combo = ComboBox(self, values, pattern)
self.setCellWidget(row, 4, values_combo)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index 784806c15e..bfe94a389f 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -35,7 +35,7 @@
- Sections to include in catalog.
+ Enabled sections will be included in the generated catalog.Included sections
@@ -107,12 +107,8 @@
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
-<html><head><meta name="qrichtext" content="1" /><style type="text/css">
-p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p>
-<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html>
+ A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.
+The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book], and '+', the default tag for a read book.Excluded genres
@@ -209,16 +205,11 @@ p, li { white-space: pre-wrap; }
- Matching books will not be included in generated catalog.
+ Books matching any of the exclusion rules will be excluded from the generated catalog. Excluded books
-
-
-
-
-
@@ -230,16 +221,11 @@ p, li { white-space: pre-wrap; }
- The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog.
+ The first enabled matching rule will be used to add a prefix to book listings in the generated catalog.Prefix rules
-
-
-
-
-
@@ -305,7 +291,7 @@ p, li { white-space: pre-wrap; }
- Size hint for Description cover thumbnails
+ Size hint for cover thumbnails included in Descriptions section. inch
@@ -362,7 +348,7 @@ p, li { white-space: pre-wrap; }
- Custom column source for note to include in Description header area
+ Custom column source for text to include in Description section.
@@ -404,7 +390,7 @@ p, li { white-space: pre-wrap; }
- Additional content merged with Comments during catalog generation
+ Custom column containing additional content to be merged with Comments metadata.
@@ -418,7 +404,7 @@ p, li { white-space: pre-wrap; }
- Merge additional content before Comments
+ Merge additional content before Comments metadata.&Before
@@ -428,7 +414,7 @@ p, li { white-space: pre-wrap; }
- Merge additional content after Comments
+ Merge additional content after Comments metadata.&After
@@ -445,7 +431,7 @@ p, li { white-space: pre-wrap; }
- Separate Comments and additional content with horizontal rule
+ Separate Comments metadata and additional content with a horizontal rule.&Separator
diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py
index 63fe02d5f1..825778f0d5 100644
--- a/src/calibre/library/catalogs/epub_mobi_builder.py
+++ b/src/calibre/library/catalogs/epub_mobi_builder.py
@@ -470,6 +470,7 @@ class CatalogBuilder(object):
return self.__output_profile.empty_ratings_char
return property(fget=fget)
@dynamic_property
+
def READ_PROGRESS_SYMBOL(self):
def fget(self):
return "▪" if self.generateForKindle else '+'
@@ -672,7 +673,7 @@ Author '{0}':
for record in data:
matched = list(set(record['tags']) & set(exclude_tags))
if matched :
- self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched))
+ self.opts.log.info(" - %s (Exclusion rule Tags: '%s')" % (record['title'], str(matched[0])))
search_phrase = ''
if exclude_tags:
@@ -4038,7 +4039,9 @@ Author '{0}':
if re.search(pat, unicode(field_contents),
re.IGNORECASE) is not None:
if self.opts.verbose:
- self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat))
+ field_md = self.db.metadata_for_field(field)
+ self.opts.log.info(" - %s (Exclusion rule '%s': %s:%s)" %
+ (record['title'], field_md['name'], field,pat))
exclusion_set.append(record)
if record in filtered_data_set:
filtered_data_set.remove(record)
From b171273f9c508332b51b72d3b1df4eee332223d3 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Mon, 6 Aug 2012 20:19:20 -0400
Subject: [PATCH 22/27] Update Qqick start guide.
---
resources/quick_start.epub | Bin 130585 -> 130580 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/resources/quick_start.epub b/resources/quick_start.epub
index 6f8f7046cd8d6cfc55ad220c6e0c381fab201f5b..a3f74213a6a374ebb271a570214af6962283c8c4 100644
GIT binary patch
delta 734
zcmbRFhkeQ)_6;wj_^RfzIUfA>x9uAv14A|QqnauEHUwF$k=QZ2au+Y$_N7$1l-k4adu^^Lw%k>+R&$8^|zP0LS`&^m&`{nDz
zp5NZoP+hWhTiS_58xAx0rcE_Ftr&fI(cd0N-%A>i5zWqNI@bNng=UrWc$iS5QB{
ze~*-Zy!wG#bLwBI&hdXHT>s-w*1s~YK#9V-kA18RI_j&~=H#2rF1XCSHu}>~_kDM{
zT`tc1^g&iDNQaNb=q%$)=|l
zSIF;K;a9RtNA5k_hhxv!(jxm(yZk=9nx9ZV(JFrZae>aYWq$g_P4Dk7xZP>x|E>DU
zo4fzZO|DALk`i6h>%=b9VpI0Z!h^egMb65Cg}kfM7WB=TrS4f>b9Y{EjR~LX%j%XR
z)0Uc=_H@kgyZ$8JyLy^;`(mf{0#%9Z>wHpWE2q259!!<_H*Ld%-~UYhelnJ3w`U0O
zW@Zs#nEwA4qr~Lv0_x3KvfHy{8BOmh$}x*DFmP}%pr#lG20aD{V4Qy77h@umg6w2R
zS%c}{e=-V8PyWp)#gu0IZ{sP%1ejxVbIzRU5)qfea7++0a_LtF_`R_Nz>GIzh#l-@=S=m6g3j?7f
MkoELCFc~ud0NQdx>Hq)$
delta 696
zcmV;p0!RIn{0Eu*2e9ZG4zZi#KD0aHcl!YV0Eq*W(GV?vFfMp#bZu;nQ`>ISKoEWR
zS4{V@!XCR35(yiZ)Ty{AltNMgs;X*jZ{t<$U28pQ5+Oc_-{8ww+bNd_D*WKf>^W!7
zT+C0mRSEafHp=KRi-M4W)S1bZUXIzC?)+yA8WK7erO|TCv|*>Q$B(A(lEvrCb12C1
zFR#vKuM=Q@KH|qgf;e4F;nSr>EcWo2
zA6+NY$zpPSjGhC&sKrvk7pR0?D($C+$W$YE8id_d)ANTTGWDPFua1t#H#f0Uav6K4
zu5|2c_jqdZ9b~0wT)(ou0jx7uLvb>=q$2SJF=%^nL$4~7YmRq2}C-{M~w-G-&jlqmgq
zA*`H~Y9+yO;Y!!WlC!b*!%@V+Z{>4rr`1gb&uwt*{evGJw;vq=d)=3s`2h?Sv^(N=
z`vCv|i30!tE&u=k000000GGS^0bv3q9g}ehDVIn40Wkt|G?Q@&DVL-B0Wku#Ig@b-
zD3|{G0Wtw;mofYSTLKP2lW_?tlTJYgm%jV~Dgus3lW_?tlg>y7mm2*6Dgrr9lW_?t
emv8+6F#+$Fq5T0v0s{A!=KTRE2I}_#0001Ms799n
From d703ef3ebef98392903bfaf0b2395d71db4aabb1 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 7 Aug 2012 09:22:32 +0530
Subject: [PATCH 23/27] Fix #1033624 (won't reconize HTC one s)
---
src/calibre/devices/android/driver.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 36dbcfafb7..12e7f40301 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -43,6 +43,7 @@ class ANDROID(USBMS):
0xccf : HTC_BCDS,
0xcd6 : HTC_BCDS,
0xce5 : HTC_BCDS,
+ 0xcec : HTC_BCDS,
0x2910 : HTC_BCDS,
0xff9 : HTC_BCDS,
},
From 08f38df9b9b3f63ba6c652ce09fb184154210e8f Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 7 Aug 2012 10:41:48 +0530
Subject: [PATCH 24/27] MOBI Output: Fix ToC at start option having no effect
when converting some input documents that have an out-of-spine ToC. Fixes
#1033656 (Table of Contents not in Front after converting)
---
src/calibre/ebooks/oeb/transforms/htmltoc.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/oeb/transforms/htmltoc.py b/src/calibre/ebooks/oeb/transforms/htmltoc.py
index 26b2ccf41c..0e1b2b5f10 100644
--- a/src/calibre/ebooks/oeb/transforms/htmltoc.py
+++ b/src/calibre/ebooks/oeb/transforms/htmltoc.py
@@ -73,7 +73,10 @@ class HTMLTOCAdder(object):
if (hasattr(item.data, 'xpath') and
XPath('//h:a[@href]')(item.data)):
if oeb.spine.index(item) < 0:
- oeb.spine.add(item, linear=False)
+ if self.position == 'end':
+ oeb.spine.add(item, linear=False)
+ else:
+ oeb.spine.insert(0, item, linear=True)
return
elif has_toc:
oeb.guide.remove('toc')
From de21068e728e912ac4e27e57fa5df5949b590c4e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 7 Aug 2012 10:47:43 +0530
Subject: [PATCH 25/27] Fix #1033430 (line scrolling do not stop at page break
in paged mode)
---
src/calibre/gui2/viewer/documentview.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py
index 18a8005ab2..0bae46d717 100644
--- a/src/calibre/gui2/viewer/documentview.py
+++ b/src/calibre/gui2/viewer/documentview.py
@@ -1036,14 +1036,14 @@ class DocumentView(QWebView): # {{{
if not self.handle_key_press(event):
return QWebView.keyPressEvent(self, event)
- def paged_col_scroll(self, forward=True):
+ def paged_col_scroll(self, forward=True, scroll_past_end=True):
dir = 'next' if forward else 'previous'
loc = self.document.javascript(
'paged_display.%s_col_location()'%dir, typ='int')
if loc > -1:
self.document.scroll_to(x=loc, y=0)
self.manager.scrolled(self.document.scroll_fraction)
- else:
+ elif scroll_past_end:
(self.manager.next_document() if forward else
self.manager.previous_document())
@@ -1059,7 +1059,8 @@ class DocumentView(QWebView): # {{{
self.is_auto_repeat_event = False
elif key == 'Down':
if self.document.in_paged_mode:
- self.paged_col_scroll()
+ self.paged_col_scroll(scroll_past_end=not
+ self.document.line_scrolling_stops_on_pagebreaks)
else:
if (not self.document.line_scrolling_stops_on_pagebreaks and
self.document.at_bottom):
@@ -1068,7 +1069,8 @@ class DocumentView(QWebView): # {{{
self.scroll_by(y=15)
elif key == 'Up':
if self.document.in_paged_mode:
- self.paged_col_scroll(forward=False)
+ self.paged_col_scroll(forward=False, scroll_past_end=not
+ self.document.line_scrolling_stops_on_pagebreaks)
else:
if (not self.document.line_scrolling_stops_on_pagebreaks and
self.document.at_top):
From 21022a6c0d337f46ceb0638909bbf05c4d3006d5 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 7 Aug 2012 09:04:46 +0200
Subject: [PATCH 26/27] Move the new collections option to the end to avoid
trouble.
---
.../devices/smart_device_app/driver.py | 32 +++++++++----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py
index 5fb9761d32..55b9deeff1 100644
--- a/src/calibre/devices/smart_device_app/driver.py
+++ b/src/calibre/devices/smart_device_app/driver.py
@@ -111,14 +111,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
ALL_BY_AUTHOR = _('All by author')
EXTRA_CUSTOMIZATION_MESSAGE = [
- _('Comma separated list of metadata fields '
- 'to turn into collections on the device. Possibilities include: ')+\
- 'series, tags, authors' +\
- _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add '
- 'these values to the list to enable them. The collections will be '
- 'given the name provided after the ":" character.')%dict(
- abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR),
- '',
_('Enable connections at startup') + ':::
' +
_('Check this box to allow connections when calibre starts') + '
',
'',
@@ -132,23 +124,31 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
_('Enter the port number the driver is to use if the "fixed port" box is checked') + '
',
_('Print extra debug information') + ':::
' +
_('Check this box if requested when reporting problems') + '
',
+ '',
+ _('Comma separated list of metadata fields '
+ 'to turn into collections on the device. Possibilities include: ')+\
+ 'series, tags, authors' +\
+ _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add '
+ 'these values to the list to enable them. The collections will be '
+ 'given the name provided after the ":" character.')%dict(
+ abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR)
]
EXTRA_CUSTOMIZATION_DEFAULT = [
- 'tags, series',
- '',
False,
'',
'',
'',
False, '9090',
False,
+ '',
+ ''
]
- OPT_COLLECTIONS = 0
- OPT_AUTOSTART = 2
- OPT_PASSWORD = 4
- OPT_USE_PORT = 6
- OPT_PORT_NUMBER = 7
- OPT_EXTRA_DEBUG = 8
+ OPT_AUTOSTART = 0
+ OPT_PASSWORD = 2
+ OPT_USE_PORT = 4
+ OPT_PORT_NUMBER = 5
+ OPT_EXTRA_DEBUG = 6
+ OPT_COLLECTIONS = 8
def __init__(self, path):
self.sync_lock = threading.RLock()
From 89472f78ec2a1bdb9ebe5a99ecc140b1a1585678 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 7 Aug 2012 13:55:58 +0530
Subject: [PATCH 27/27] Generate a PDF version of the User Manual
---
manual/Makefile | 2 +-
manual/conf.py | 17 +++++++++++++----
manual/custom.py | 2 ++
manual/index.rst | 2 +-
manual/latex.py | 25 +++++++++++++++++++++++++
setup/publish.py | 9 +++++++++
6 files changed, 51 insertions(+), 6 deletions(-)
create mode 100644 manual/latex.py
diff --git a/manual/Makefile b/manual/Makefile
index c1a2279abf..a21de12bed 100644
--- a/manual/Makefile
+++ b/manual/Makefile
@@ -60,7 +60,7 @@ htmlhelp:
latex:
mkdir -p .build/latex .build/doctrees
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex
+ $(SPHINXBUILD) -b mylatex $(ALLSPHINXOPTS) .build/latex
@echo
@echo "Build finished; the LaTeX files are in .build/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
diff --git a/manual/conf.py b/manual/conf.py
index 7b24f2f50a..967b6f0c65 100644
--- a/manual/conf.py
+++ b/manual/conf.py
@@ -14,10 +14,10 @@
import sys, os
# If your extensions are in another directory, add it here.
-sys.path.append(os.path.abspath('../src'))
sys.path.append(os.path.abspath('.'))
-__appname__ = os.environ.get('__appname__', 'calibre')
-__version__ = os.environ.get('__version__', '0.0.0')
+import init_calibre
+init_calibre
+from calibre.constants import __appname__, __version__
import custom
custom
# General configuration
@@ -154,7 +154,8 @@ latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, document class [howto/manual]).
-#latex_documents = []
+latex_documents = [('index', 'calibre.tex', 'calibre User Manual',
+ 'Kovid Goyal', 'manual', False)]
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
@@ -164,3 +165,11 @@ latex_font_size = '10pt'
# If false, no module index is generated.
#latex_use_modindex = True
+
+latex_logo = 'resources/logo.png'
+latex_show_pagerefs = True
+latex_show_urls = 'footnote'
+latex_elements = {
+'papersize':'letterpaper',
+'fontenc':r'\usepackage[T2A,T1]{fontenc}'
+}
diff --git a/manual/custom.py b/manual/custom.py
index fdfb5711bb..30ca28ec96 100644
--- a/manual/custom.py
+++ b/manual/custom.py
@@ -14,6 +14,7 @@ from sphinx.util.console import bold
sys.path.append(os.path.abspath('../../../'))
from calibre.linux import entry_points
from epub import EPUBHelpBuilder
+from latex import LaTeXHelpBuilder
def substitute(app, doctree):
pass
@@ -251,6 +252,7 @@ def template_docs(app):
def setup(app):
app.add_config_value('kovid_epub_cover', None, False)
app.add_builder(EPUBHelpBuilder)
+ app.add_builder(LaTeXHelpBuilder)
app.connect('doctree-read', substitute)
app.connect('builder-inited', generate_docs)
app.connect('build-finished', finished)
diff --git a/manual/index.rst b/manual/index.rst
index fa89dba95f..b8f98a5561 100755
--- a/manual/index.rst
+++ b/manual/index.rst
@@ -17,7 +17,7 @@ To get started with more advanced usage, you should read about the :ref:`Graphic
.. only:: online
- **An ebook version of this user manual is available in** `EPUB format `_ and `AZW3 (Kindle Fire) format `_.
+ **An ebook version of this user manual is available in** `EPUB format `_, `AZW3 (Kindle Fire) format `_ and `PDF format `_.
Sections
------------
diff --git a/manual/latex.py b/manual/latex.py
new file mode 100644
index 0000000000..95f38eab20
--- /dev/null
+++ b/manual/latex.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import os
+
+
+from sphinx.builders.latex import LaTeXBuilder
+
+class LaTeXHelpBuilder(LaTeXBuilder):
+ name = 'mylatex'
+
+ def finish(self):
+ LaTeXBuilder.finish(self)
+ self.info('Fixing Cyrillic characters...')
+ tex = os.path.join(self.outdir, 'calibre.tex')
+ with open(tex, 'r+b') as f:
+ raw = f.read().replace(b'Михаил Горбачёв',
+ br'{\fontencoding{T2A}\selectfont Михаил Горбачёв}')
+ f.seek(0)
+ f.write(raw)
diff --git a/setup/publish.py b/setup/publish.py
index e43c9fdf7f..fd0dd48900 100644
--- a/setup/publish.py
+++ b/setup/publish.py
@@ -80,8 +80,17 @@ class Manual(Command):
'-d', '.build/doctrees', '.', '.build/html'])
subprocess.check_call(['sphinx-build', '-b', 'myepub', '-d',
'.build/doctrees', '.', '.build/epub'])
+ subprocess.check_call(['sphinx-build', '-b', 'mylatex', '-d',
+ '.build/doctrees', '.', '.build/latex'])
+ pwd = os.getcwdu()
+ os.chdir('.build/latex')
+ subprocess.check_call(['make', 'all-pdf'], stdout=open(os.devnull,
+ 'wb'))
+ os.chdir(pwd)
epub_dest = self.j('.build', 'html', 'calibre.epub')
+ pdf_dest = self.j('.build', 'html', 'calibre.pdf')
shutil.copyfile(self.j('.build', 'epub', 'calibre.epub'), epub_dest)
+ shutil.copyfile(self.j('.build', 'latex', 'calibre.pdf'), pdf_dest)
subprocess.check_call(['ebook-convert', epub_dest,
epub_dest.rpartition('.')[0] + '.azw3',
'--page-breaks-before=/', '--disable-font-rescaling',