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 &notifications 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'))