From fa68736f6ae77b1b3ecf993bfccf94af177ae674 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 17 Aug 2010 12:31:34 +0100
Subject: [PATCH 01/11] Port bulk tags update to custom columns
---
src/calibre/gui2/custom_column_widgets.py | 55 ++++++++-------
src/calibre/library/custom_columns.py | 81 +++++++++++++++++++++++
2 files changed, 112 insertions(+), 24 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 3ed7d0c4ad..d624d5320d 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -394,30 +394,18 @@ class BulkBase(Base):
ans = list(ans)
return ans
- def process_each_book(self):
- return False
-
def initialize(self, book_ids):
- if not self.process_each_book():
- self.initial_val = val = self.get_initial_value(book_ids)
- val = self.normalize_db_val(val)
- self.setter(val)
+ self.initial_val = val = self.get_initial_value(book_ids)
+ val = self.normalize_db_val(val)
+ self.setter(val)
def commit(self, book_ids, notify=False):
- if self.process_each_book():
+ val = self.getter()
+ val = self.normalize_ui_val(val)
+ if val != self.initial_val:
for book_id in book_ids:
QCoreApplication.processEvents()
- val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
- new_val = self.getter(val)
- if set(val) != new_val:
- self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify)
- else:
- val = self.getter()
- val = self.normalize_ui_val(val)
- if val != self.initial_val:
- for book_id in book_ids:
- QCoreApplication.processEvents()
- self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
+ self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
class BulkBool(BulkBase, Bool):
pass
@@ -474,9 +462,6 @@ class BulkSeries(BulkBase):
self.db.set_custom(book_id, val, extra=s_index,
num=self.col_id, notify=notify)
- def process_each_book(self):
- return True
-
class RemoveTags(QWidget):
def __init__(self, parent, values):
@@ -539,8 +524,30 @@ class BulkText(BulkBase):
if idx is not None:
self.widgets[1].setCurrentIndex(idx)
- def process_each_book(self):
- return self.col_metadata['is_multiple']
+ def commit(self, book_ids, notify=False):
+ if self.col_metadata['is_multiple']:
+ remove = set()
+ if self.removing_widget.checkbox.isChecked():
+ for book_id in book_ids:
+ remove |= set(self.db.get_custom(book_id, num=self.col_id,
+ index_is_id=True))
+ else:
+ txt = unicode(self.removing_widget.tags_box.text())
+ if txt:
+ remove = set([v.strip() for v in txt.split(',')])
+ txt = unicode(self.adding_widget.text())
+ if txt:
+ add = set([v.strip() for v in txt.split(',')])
+ else:
+ add = set()
+ self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id)
+ else:
+ val = self.getter()
+ val = self.normalize_ui_val(val)
+ if val != self.initial_val:
+ for book_id in book_ids:
+ QCoreApplication.processEvents()
+ self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
def getter(self, original_value = None):
if self.col_metadata['is_multiple']:
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index b8e0f8d3b6..3b4a84af4f 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -313,6 +313,87 @@ class CustomColumns(object):
self.conn.commit()
return changed
+ def set_custom_bulk(self, ids, add=[], remove=[],
+ label=None, num=None, notify=False):
+ if label is not None:
+ data = self.custom_column_label_map[label]
+ if num is not None:
+ data = self.custom_column_num_map[num]
+ if not data['editable']:
+ raise ValueError('Column %r is not editable'%data['label'])
+ if data['datatype'] != 'text' or not data['is_multiple']:
+ raise ValueError('Column %r is not text/multiple'%data['label'])
+
+ add = self.cleanup_tags(add)
+ remove = self.cleanup_tags(remove)
+ remove = set(remove) - set(add)
+ if not ids or (not add and not remove):
+ return
+ # get custom table names
+ cust_table, link_table = self.custom_table_names(data['num'])
+
+ # Add tags that do not already exist into the custom cust_table
+ all_tags = self.all_custom(num=data['num'])
+ lt = [t.lower() for t in all_tags]
+ new_tags = [t for t in add if t.lower() not in lt]
+ if new_tags:
+ self.conn.executemany('INSERT INTO %s(value) VALUES (?)'%cust_table,
+ [(x,) for x in new_tags])
+
+ # Create the temporary temp_tables to store the ids for books and tags
+ # to be operated on
+ temp_tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add',
+ 'temp_bulk_tag_edit_remove')
+ drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in temp_tables])
+ creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t
+ for t in temp_tables])
+ self.conn.executescript(drops + creates)
+
+ # Populate the books temp cust_table
+ self.conn.executemany(
+ 'INSERT INTO temp_bulk_tag_edit_books VALUES (?)',
+ [(x,) for x in ids])
+
+ # Populate the add/remove tags temp temp_tables
+ for table, tags in enumerate([add, remove]):
+ if not tags:
+ continue
+ table = temp_tables[table+1]
+ insert = ('INSERT INTO {tt}(id) SELECT {ct}.id FROM {ct} WHERE value=?'
+ ' COLLATE PYNOCASE LIMIT 1').format(tt=table, ct=cust_table)
+ self.conn.executemany(insert, [(x,) for x in tags])
+
+ # now do the real work -- removing and adding the tags
+ if remove:
+ self.conn.execute(
+ '''DELETE FROM %s WHERE
+ book IN (SELECT id FROM %s) AND
+ value IN (SELECT id FROM %s)'''
+ % (link_table, temp_tables[0], temp_tables[2]))
+ if add:
+ self.conn.execute(
+ '''
+ INSERT INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2}
+ '''.format(link_table, temp_tables[0], temp_tables[1])
+ )
+ # get rid of the temp tables
+ self.conn.executescript(drops)
+ # Remove any dreg tags -- ones with no references
+ self.conn.execute(
+ '''DELETE FROM %s WHERE (SELECT COUNT(id) FROM %s WHERE
+ value=%s.id) < 1''' % (cust_table, link_table, cust_table))
+ self.conn.commit()
+
+ # set the in-memory copies of the tags
+ for x in ids:
+ tags = self.conn.get(
+ 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
+ (x,), all=False)
+ self.data.set(x, self.FIELD_MAP[data['num']], tags, row_is_id=True)
+
+ if notify:
+ self.notify('metadata', ids)
+
def set_custom(self, id_, val, label=None, num=None,
append=False, notify=True, extra=None):
if label is not None:
From 35b0faf843ea9f58e50596bc730c4fcdb5019dee Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 17 Aug 2010 13:31:31 +0100
Subject: [PATCH 02/11] Add a restriction argument to the content server
command line. Includes changes to make the restriction work.
---
src/calibre/library/server/base.py | 4 +++-
src/calibre/library/server/cache.py | 2 +-
src/calibre/library/server/main.py | 6 +++++-
src/calibre/library/server/mobile.py | 3 ++-
src/calibre/library/server/xml.py | 3 ++-
5 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index 0097276348..57e5e702fa 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -57,13 +57,15 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
server_name = __appname__ + '/' + __version__
- def __init__(self, db, opts, embedded=False, show_tracebacks=True):
+ def __init__(self, db, opts, embedded=False, show_tracebacks=True,
+ ignore_search_restriction=True):
self.db = db
for item in self.db:
item
break
self.opts = opts
self.embedded = embedded
+ self.ignore_search_restriction=ignore_search_restriction
self.state_callback = None
self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x'))
diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py
index 9fec2c2737..6f6c21e60c 100644
--- a/src/calibre/library/server/cache.py
+++ b/src/calibre/library/server/cache.py
@@ -18,7 +18,7 @@ class Cache(object):
old = self._search_cache.pop(search, None)
if old is None or old[0] <= self.db.last_modified():
matches = self.db.data.search(search, return_matches=True,
- ignore_search_restriction=True)
+ ignore_search_restriction=self.ignore_search_restriction)
if not matches:
matches = []
self._search_cache[search] = (utcnow(), frozenset(matches))
diff --git a/src/calibre/library/server/main.py b/src/calibre/library/server/main.py
index 5ca82c6b98..2fad001a86 100644
--- a/src/calibre/library/server/main.py
+++ b/src/calibre/library/server/main.py
@@ -32,6 +32,8 @@ def option_parser():
help=_('Write process PID to the specified file'))
parser.add_option('--daemonize', default=False, action='store_true',
help='Run process in background as a daemon. No effect on windows.')
+ parser.add_option('--restriction', default=None,
+ help='Specifies a restriction to be used for this invocation.')
return parser
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
@@ -83,7 +85,9 @@ def main(args=sys.argv):
if opts.with_library is None:
opts.with_library = prefs['library_path']
db = LibraryDatabase2(opts.with_library)
- server = LibraryServer(db, opts)
+ server = LibraryServer(db, opts, ignore_search_restriction=False)
+ if opts.restriction:
+ db.data.set_search_restriction('search:' + opts.restriction)
server.start()
return 0
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index c3667a2077..391ad70bfd 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -181,7 +181,8 @@ class MobileServer(object):
num = int(num)
except ValueError:
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
- ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
+ ids = self.db.search(search, return_matches=True,
+ ignore_search_restriction=self.ignore_search_restriction)
FM = self.db.FIELD_MAP
items = [r for r in iter(self.db) if r[FM['id']] in ids]
if sort is not None:
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index 036a2051bf..5649208036 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -45,7 +45,8 @@ class XMLServer(object):
order = order.lower().strip() == 'ascending'
- ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
+ ids = self.db.search(search, return_matches=True,
+ ignore_search_restriction=self.ignore_search_restriction)
FM = self.db.FIELD_MAP
From 61dd7b9e9ada322270a9c55c8d878051cb218568 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 17 Aug 2010 21:14:27 +0100
Subject: [PATCH 03/11] Add restriction to content server
---
src/calibre/gui2/dialogs/config/__init__.py | 2 ++
src/calibre/gui2/dialogs/config/config.ui | 20 +++++++++++++
src/calibre/library/caches.py | 32 ++++++++++-----------
src/calibre/library/database2.py | 1 +
src/calibre/library/server/base.py | 14 +++++++--
src/calibre/library/server/cache.py | 3 +-
src/calibre/library/server/main.py | 7 ++---
src/calibre/library/server/mobile.py | 3 +-
src/calibre/library/server/xml.py | 4 +--
9 files changed, 55 insertions(+), 31 deletions(-)
diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index 20c7843aa8..c5b61f146d 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -447,6 +447,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.password.setText(opts.password if opts.password else '')
self.opt_max_opds_items.setValue(opts.max_opds_items)
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
+ self.opt_restriction.setText(self.db.prefs.get('cs_restriction', ''))
self.auto_launch.setChecked(config['autolaunch_server'])
self.systray_icon.setChecked(config['systray_icon'])
self.sync_news.setChecked(config['upload_news_to_device'])
@@ -906,6 +907,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
sc.set('max_opds_items', self.opt_max_opds_items.value())
sc.set('max_opds_ungrouped_items',
self.opt_max_opds_ungrouped_items.value())
+ self.db.prefs.set('cs_restriction', unicode(self.opt_restriction.text()))
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index 1359278512..62eb7bb620 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -1040,6 +1040,26 @@
+ -
+
+
+ Provides a restriction to be used by the content server
+
+
+
+
+
+
+ -
+
+
+ Restriction (saved search) to apply:
+
+
+ opt_restriction
+
+
+
-
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index fa07ed8b83..ca66d28ddb 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -609,26 +609,24 @@ class ResultCache(SearchQueryParser):
self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered]
- def search(self, query, return_matches=False,
- ignore_search_restriction=False):
- q = ''
- if not query or not query.strip():
- if not ignore_search_restriction:
- q = self.search_restriction
- else:
- q = query
- if not ignore_search_restriction and self.search_restriction:
- q = u'%s (%s)' % (self.search_restriction, query)
- if not q:
- if return_matches:
- return list(self._map) # when return_matches, do not update the maps!
- self._map_filtered = list(self._map)
- return
- matches = sorted(self.parse(q))
- ans = [id for id in self._map if id in matches]
+ def search(self, query, return_matches=False):
+ ans = self.search_getting_ids(query, self.search_restriction)
if return_matches:
return ans
self._map_filtered = ans
+ def search_getting_ids(self, query, search_restriction):
+ q = ''
+ if not query or not query.strip():
+ q = search_restriction
+ else:
+ q = query
+ if search_restriction:
+ q = u'%s (%s)' % (search_restriction, query)
+ if not q:
+ return list(self._map)
+ matches = sorted(self.parse(q))
+ return [id for id in self._map if id in matches]
+
def set_search_restriction(self, s):
self.search_restriction = s
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index b8ac065760..6728735eb3 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -296,6 +296,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
self.search = self.data.search
+ self.search_getting_ids = self.data.search_getting_ids
self.refresh = functools.partial(self.data.refresh, self)
self.sort = self.data.sort
self.index = self.data.index
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index 57e5e702fa..bdaee1c42d 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -57,15 +57,13 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
server_name = __appname__ + '/' + __version__
- def __init__(self, db, opts, embedded=False, show_tracebacks=True,
- ignore_search_restriction=True):
+ def __init__(self, db, opts, embedded=False, show_tracebacks=True):
self.db = db
for item in self.db:
item
break
self.opts = opts
self.embedded = embedded
- self.ignore_search_restriction=ignore_search_restriction
self.state_callback = None
self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x'))
@@ -97,9 +95,19 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
}
+ self.set_search_restriction(db.prefs.get('cs_restriction', ''))
+ if opts.restriction is not None:
+ self.set_search_restriction(opts.restriction)
+
self.is_running = False
self.exception = None
+ def set_search_restriction(self, restriction):
+ if restriction:
+ self.search_restriction = 'search:'+restriction
+ else:
+ self.search_restriction = ''
+
def setup_loggers(self):
access_file = log_access_file
error_file = log_error_file
diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py
index 6f6c21e60c..94e4a1c041 100644
--- a/src/calibre/library/server/cache.py
+++ b/src/calibre/library/server/cache.py
@@ -17,8 +17,7 @@ class Cache(object):
def search_cache(self, search):
old = self._search_cache.pop(search, None)
if old is None or old[0] <= self.db.last_modified():
- matches = self.db.data.search(search, return_matches=True,
- ignore_search_restriction=self.ignore_search_restriction)
+ matches = self.db.data.search_getting_ids(search, self.search_restriction)
if not matches:
matches = []
self._search_cache[search] = (utcnow(), frozenset(matches))
diff --git a/src/calibre/library/server/main.py b/src/calibre/library/server/main.py
index 2fad001a86..e87d2d75d7 100644
--- a/src/calibre/library/server/main.py
+++ b/src/calibre/library/server/main.py
@@ -33,7 +33,8 @@ def option_parser():
parser.add_option('--daemonize', default=False, action='store_true',
help='Run process in background as a daemon. No effect on windows.')
parser.add_option('--restriction', default=None,
- help='Specifies a restriction to be used for this invocation.')
+ help=_('Specifies a restriction to be used for this invocation. '
+ 'This option overrides the default set in the GUI'))
return parser
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
@@ -85,9 +86,7 @@ def main(args=sys.argv):
if opts.with_library is None:
opts.with_library = prefs['library_path']
db = LibraryDatabase2(opts.with_library)
- server = LibraryServer(db, opts, ignore_search_restriction=False)
- if opts.restriction:
- db.data.set_search_restriction('search:' + opts.restriction)
+ server = LibraryServer(db, opts)
server.start()
return 0
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index 391ad70bfd..8ea9a04515 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -181,8 +181,7 @@ class MobileServer(object):
num = int(num)
except ValueError:
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
- ids = self.db.search(search, return_matches=True,
- ignore_search_restriction=self.ignore_search_restriction)
+ ids = self.db.search_getting_ids(search, self.search_restriction)
FM = self.db.FIELD_MAP
items = [r for r in iter(self.db) if r[FM['id']] in ids]
if sort is not None:
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index 5649208036..18370ab641 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -45,8 +45,7 @@ class XMLServer(object):
order = order.lower().strip() == 'ascending'
- ids = self.db.search(search, return_matches=True,
- ignore_search_restriction=self.ignore_search_restriction)
+ ids = self.db.search_getting_ids(search, self.search_restriction)
FM = self.db.FIELD_MAP
@@ -54,7 +53,6 @@ class XMLServer(object):
if sort is not None:
self.sort(items, sort, order)
-
books = []
def serialize(x):
From 037a377a1cc23048842517493e5d5107d43b654a Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 17 Aug 2010 21:23:28 +0100
Subject: [PATCH 04/11] Remove search restriction tweak
---
resources/default_tweaks.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index fe117ce586..d6f134f724 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -81,8 +81,3 @@ title_series_sorting = 'library_order'
# strictly_alphabetic, it would remain "The Client".
save_template_title_series_sorting = 'library_order'
-# Specify a restriction to apply when calibre starts or when change library is
-# used. Provide the name of a saved search. It is ignored if the saved search
-# does not exist in the library being opened. The value '' means no restriction.
-restrict_at_startup = ''
-
From 73de2ae89126eb060edceb1e30e4584f94c615c3 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 17 Aug 2010 21:47:50 +0100
Subject: [PATCH 05/11] Remove empty categories from the opds version
---
src/calibre/library/server/opds.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index f32e5ad47a..e1cbb79599 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -550,6 +550,8 @@ class OPDSServer(object):
(_('Title'), _('Title'), 'Otitle'),
]
for category in categories:
+ if len(categories[category]) == 0:
+ continue
if category == 'formats':
continue
meta = category_meta.get(category, None)
From 2464da207eadf16759352c256bdd988ab262d200 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 06:58:08 +0100
Subject: [PATCH 06/11] Ensure restrictions with spaces in the name work in
content server.
---
src/calibre/library/server/base.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index bdaee1c42d..ad1cd5b13a 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -104,7 +104,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
def set_search_restriction(self, restriction):
if restriction:
- self.search_restriction = 'search:'+restriction
+ self.search_restriction = 'search:"%s"'%restriction
else:
self.search_restriction = ''
From d3e2c90a231c342f522d9a99ce998a1b98a011fc Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 08:08:28 +0100
Subject: [PATCH 07/11] Add bulk setting of the rest of the custom column types
---
src/calibre/gui2/custom_column_widgets.py | 20 ++++++++--------
src/calibre/library/custom_columns.py | 29 +++++++++++++++++++----
2 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index d624d5320d..8108454db3 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -403,9 +403,7 @@ class BulkBase(Base):
val = self.getter()
val = self.normalize_ui_val(val)
if val != self.initial_val:
- for book_id in book_ids:
- QCoreApplication.processEvents()
- self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
+ self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
class BulkBool(BulkBase, Bool):
pass
@@ -448,18 +446,21 @@ class BulkSeries(BulkBase):
val = self.normalize_ui_val(val)
update_indices = self.idx_widget.checkState()
if val != '':
+ extras = []
+ next_index = self.db.get_next_cc_series_num_for(val, num=self.col_id)
for book_id in book_ids:
QCoreApplication.processEvents()
if update_indices:
if tweaks['series_index_auto_increment'] == 'next':
- s_index = self.db.get_next_cc_series_num_for\
- (val, num=self.col_id)
+ s_index = next_index
+ next_index += 1
else:
s_index = 1.0
else:
s_index = self.db.get_custom_extra(book_id, num=self.col_id,
index_is_id=True)
- self.db.set_custom(book_id, val, extra=s_index,
+ extras.append(s_index)
+ self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify)
class RemoveTags(QWidget):
@@ -540,14 +541,13 @@ class BulkText(BulkBase):
add = set([v.strip() for v in txt.split(',')])
else:
add = set()
- self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id)
+ self.db.set_custom_bulk_multiple(book_ids, add=add, remove=remove,
+ num=self.col_id)
else:
val = self.getter()
val = self.normalize_ui_val(val)
if val != self.initial_val:
- for book_id in book_ids:
- QCoreApplication.processEvents()
- self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
+ self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
def getter(self, original_value = None):
if self.col_metadata['is_multiple']:
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index 7c613295b9..f02294a102 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -313,7 +313,7 @@ class CustomColumns(object):
self.conn.commit()
return changed
- def set_custom_bulk(self, ids, add=[], remove=[],
+ def set_custom_bulk_multiple(self, ids, add=[], remove=[],
label=None, num=None, notify=False):
'''
Fast algorithm for updating custom column is_multiple datatypes.
@@ -394,7 +394,30 @@ class CustomColumns(object):
if notify:
self.notify('metadata', ids)
- def set_custom(self, id_, val, label=None, num=None,
+ def set_custom_bulk(self, ids, val, label=None, num=None,
+ append=False, notify=True, extras=None):
+ '''
+ Change the value of a column for a set of books. The ids parameter is a
+ list of book ids to change. The extra field must be None or a list the
+ same length as ids.
+ '''
+ if extras is not None and len(extras) != len(ids):
+ raise ValueError('Lentgh of ids and extras is not the same')
+ ev = None
+ for idx,id in enumerate(ids):
+ if extras is not None:
+ ev = extras[idx]
+ self._set_custom(id, val, label=label, num=num, append=append,
+ notify=notify, extra=ev)
+ self.conn.commit()
+
+ def set_custom(self, id, val, label=None, num=None,
+ append=False, notify=True, extra=None):
+ self._set_custom(id, val, label=label, num=num, append=append,
+ notify=notify, extra=extra)
+ self.conn.commit()
+
+ def _set_custom(self, id_, val, label=None, num=None,
append=False, notify=True, extra=None):
if label is not None:
data = self.custom_column_label_map[label]
@@ -450,7 +473,6 @@ class CustomColumns(object):
self.conn.execute(
'''INSERT INTO %s(book, value)
VALUES (?,?)'''%lt, (id_, xid))
- self.conn.commit()
nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(id_,), all=False)
@@ -462,7 +484,6 @@ class CustomColumns(object):
self.conn.execute(
'INSERT INTO %s(book,value) VALUES (?,?)'%table,
(id_, val))
- self.conn.commit()
nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(id_,), all=False)
From fa17a92c2c97f264f2919c55aa271b780dc76da6 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 10:11:11 +0100
Subject: [PATCH 08/11] 1) add restriction on startup to GUI 2) fix sorting
problem in restriction combobx
---
src/calibre/gui2/dialogs/config/__init__.py | 12 ++++-
src/calibre/gui2/dialogs/config/config.ui | 50 ++++++++++++++-------
src/calibre/gui2/search_box.py | 3 +-
src/calibre/gui2/ui.py | 2 +
4 files changed, 48 insertions(+), 19 deletions(-)
diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index c5b61f146d..2caaabc2cc 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -36,6 +36,7 @@ from calibre.gui2.convert.structure_detection import StructureDetectionWidget
from calibre.ebooks.conversion.plumber import Plumber
from calibre.utils.logging import Log
from calibre.gui2.convert.toc import TOCWidget
+from calibre.utils.search_query_parser import saved_searches
class ConfigTabs(QTabWidget):
@@ -447,7 +448,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.password.setText(opts.password if opts.password else '')
self.opt_max_opds_items.setValue(opts.max_opds_items)
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
- self.opt_restriction.setText(self.db.prefs.get('cs_restriction', ''))
+ self.opt_cs_restriction.setText(self.db.prefs.get('cs_restriction', ''))
self.auto_launch.setChecked(config['autolaunch_server'])
self.systray_icon.setChecked(config['systray_icon'])
self.sync_news.setChecked(config['upload_news_to_device'])
@@ -494,6 +495,12 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
if x == config['gui_layout']:
li = i
self.opt_gui_layout.setCurrentIndex(li)
+ restrictions = sorted(saved_searches().names(),
+ cmp=lambda x,y: cmp(x.lower(), y.lower()))
+ restrictions.insert(0, '')
+ self.opt_gui_restriction.addItems(restrictions)
+ idx = self.opt_gui_restriction.findText(self.db.prefs.get('gui_restriction', ''))
+ self.opt_gui_restriction.setCurrentIndex(0 if idx < 0 else idx)
self.opt_disable_animations.setChecked(config['disable_animations'])
self.opt_show_donate_button.setChecked(config['show_donate_button'])
idx = 0
@@ -907,7 +914,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
sc.set('max_opds_items', self.opt_max_opds_items.value())
sc.set('max_opds_ungrouped_items',
self.opt_max_opds_ungrouped_items.value())
- self.db.prefs.set('cs_restriction', unicode(self.opt_restriction.text()))
+ self.db.prefs.set('cs_restriction', unicode(self.opt_cs_restriction.text()))
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()
@@ -929,6 +936,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['internally_viewed_formats'] = fmts
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
config['gui_layout'] = unicode(val)
+ self.db.prefs.set('gui_restriction', unicode(self.opt_gui_restriction.currentText()))
if must_restart:
warning_dialog(self, _('Must restart'),
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index 62eb7bb620..3c5c109e04 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -295,7 +295,7 @@
-
-
+
-
Use &Roman numerals for series number
@@ -305,35 +305,35 @@
- -
+
-
Enable system &tray icon (needs restart)
- -
+
-
Show ¬ifications in system tray
- -
+
-
Show &splash screen at startup
- -
+
-
Show cover &browser in a separate window (needs restart)
- -
+
-
Show &average ratings in the tags browser
@@ -343,7 +343,7 @@
- -
+
-
Search as you type
@@ -353,21 +353,21 @@
- -
+
-
Automatically send downloaded &news to ebook reader
- -
+
-
&Delete news from library when it is automatically sent to reader
- -
+
-
-
@@ -384,7 +384,7 @@
- -
+
-
-
@@ -570,7 +570,27 @@
+ -
+
+
+ Restriction to apply when the current library is opened (startup or change library):
+
+
+ opt_gui_restriction
+
+
+
-
+
+
+
+ 250
+ 16777215
+
+
+
+
+ -
Disable all animations. Useful if you have a slow/old computer.
@@ -580,14 +600,14 @@
- -
+
-
Show &donate button (restart)
- -
+
-
&Toolbar
@@ -1041,7 +1061,7 @@
-
-
+
Provides a restriction to be used by the content server
@@ -1056,7 +1076,7 @@
Restriction (saved search) to apply:
- opt_restriction
+ opt_cs_restriction
diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py
index 95667379a1..7169eb5fd3 100644
--- a/src/calibre/gui2/search_box.py
+++ b/src/calibre/gui2/search_box.py
@@ -402,8 +402,7 @@ class SavedSearchBoxMixin(object):
b.setStatusTip(b.toolTip())
def saved_searches_changed(self):
- p = saved_searches().names()
- p.sort()
+ p = sorted(saved_searches().names(), cmp=lambda x,y: cmp(x.lower(), y.lower()))
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
self.search_restriction.addItem('')
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index f521bceaf9..41b166b13f 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -230,6 +230,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
+ self.apply_named_search_restriction(db.prefs.get('gui_restriction', ''))
########################### Cover Flow ################################
@@ -373,6 +374,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed() # reload the search restrictions combo box
+ self.apply_named_search_restriction(db.prefs.get('gui_restriction', ''))
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
From 8521a4b6989d1c06a35c73617753d2a39dee76c8 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 10:56:40 +0100
Subject: [PATCH 09/11] Fix #6537 - series order on sony being lost
---
src/calibre/devices/usbms/books.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index cdba980642..2b304da27f 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -181,7 +181,7 @@ class CollectionsBookList(BookList):
if lpath not in collections_lpaths[category]:
collections_lpaths[category].add(lpath)
collections[category].append(book)
- if attr == 'series':
+ if attr == 'series' or getattr(book, 'series', None) == category:
series_categories.add(category)
# Sort collections
for category, books in collections.items():
From e2b093f6ab039027515ce1bfbb2180c58818394c Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 11:23:19 +0100
Subject: [PATCH 10/11] Improve the fix for #6537 - series order on sony being
lost
---
src/calibre/devices/usbms/books.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 2b304da27f..959f26199c 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -181,7 +181,9 @@ class CollectionsBookList(BookList):
if lpath not in collections_lpaths[category]:
collections_lpaths[category].add(lpath)
collections[category].append(book)
- if attr == 'series' or getattr(book, 'series', None) == category:
+ if attr == 'series' or \
+ ('series' in collection_attributes and
+ getattr(book, 'series', None) == category):
series_categories.add(category)
# Sort collections
for category, books in collections.items():
From 0fcf26a113c416e0af95b316b99f6634be72d312 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 18 Aug 2010 13:09:59 +0100
Subject: [PATCH 11/11] Enhancement #6237 - automatically connect to a folder
on startup
---
resources/default_tweaks.py | 8 ++++++++
src/calibre/gui2/device.py | 9 ++++++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index d6f134f724..07aee5c6fa 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -81,3 +81,11 @@ title_series_sorting = 'library_order'
# strictly_alphabetic, it would remain "The Client".
save_template_title_series_sorting = 'library_order'
+# Specify a folder that calibre should connect to at startup using
+# connect_to_folder. This must be a full path to the folder. If the folder does
+# not exist when calibre starts, it is ignored. If there are '\' characters in
+# the path (such as in Windows paths), you must double them.
+# Examples:
+# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
+# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
+auto_connect_to_folder = ''
\ No newline at end of file
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index a9beb317a2..6791c4b015 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -33,7 +33,7 @@ from calibre.devices.apple.driver import ITUNES_ASYNC
from calibre.devices.folder_device.driver import FOLDER_DEVICE
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import DEBUG
-from calibre.utils.config import prefs
+from calibre.utils.config import prefs, tweaks
# }}}
@@ -613,6 +613,8 @@ class DeviceMixin(object): # {{{
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
self.job_manager, Dispatcher(self.status_bar.show_message))
self.device_manager.start()
+ if tweaks['auto_connect_to_folder']:
+ self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
def set_default_thumbnail(self, height):
r = QSvgRenderer(I('book.svg'))
@@ -624,6 +626,11 @@ class DeviceMixin(object): # {{{
self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap))
+ def connect_to_folder_named(self, dir):
+ if os.path.isdir(dir):
+ kls = FOLDER_DEVICE
+ self.device_manager.mount_device(kls=kls, kind='folder', path=dir)
+
def connect_to_folder(self):
dir = choose_dir(self, 'Select Device Folder',
_('Select folder to open as device'))