From b540537f30019ed965d4f7e74e0850f5054de315 Mon Sep 17 00:00:00 2001 From: Spedinfargo Date: Fri, 25 Feb 2011 16:05:02 -0600 Subject: [PATCH 1/9] Suggested patch for max issues per recipe --- src/calibre/gui2/actions/fetch_news.py | 10 +++++++ src/calibre/gui2/dialogs/scheduler.py | 14 +++++++--- src/calibre/gui2/dialogs/scheduler.ui | 30 +++++++++++++++++++++ src/calibre/library/database2.py | 12 +++++++++ src/calibre/web/feeds/recipes/collection.py | 9 +++++-- src/calibre/web/feeds/recipes/model.py | 5 ++-- 6 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index 5c2a5e9663..009bf8b00b 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -58,6 +58,16 @@ class FetchNewsAction(InterfaceAction): self.scheduler.recipe_download_failed(arg) return self.gui.job_exception(job) id = self.gui.library_view.model().add_news(pt.name, arg) + + # Arg may contain a "keep_issues" variable. if it is non-zer, delete all but newest x issues. + try: + ikeep_issues = int(arg['keep_issues']) + except: + ikeep_issues = 0 + if ikeep_issues > 0: + ids2delete = self.gui.library_view.model().db.get_most_recent_by_tag(arg['keep_issues'], arg['title']) + self.gui.library_view.model().delete_books_by_id(ids2delete) + self.gui.library_view.model().reset() sync = self.gui.news_to_be_synced sync.add(id) diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index b6a3bed3eb..be2997a314 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -153,9 +153,10 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.recipe_model.un_schedule_recipe(urn) add_title_tag = self.add_title_tag.isChecked() + keep_issues = unicode(self.keep_issues.text()) custom_tags = unicode(self.custom_tags.text()).strip() custom_tags = [x.strip() for x in custom_tags.split(',')] - self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags) + self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags, keep_issues) return True def initialize_detail_box(self, urn): @@ -215,9 +216,15 @@ class SchedulerDialog(QDialog, Ui_Dialog): if d < timedelta(days=366): self.last_downloaded.setText(_('Last downloaded')+': '+tm) - add_title_tag, custom_tags = customize_info + add_title_tag, custom_tags, keep_issues = customize_info self.add_title_tag.setChecked(add_title_tag) self.custom_tags.setText(u', '.join(custom_tags)) + try: + ikeep_issues = int(keep_issues) + except: + ikeep_issues = 0 + self.keep_issues.setValue(ikeep_issues) + class Scheduler(QObject): @@ -299,7 +306,7 @@ class Scheduler(QObject): un = pw = None if account_info is not None: un, pw = account_info - add_title_tag, custom_tags = customize_info + add_title_tag, custom_tags, keep_issues = customize_info script = self.recipe_model.get_recipe(urn) pt = PersistentTemporaryFile('_builtin.recipe') pt.write(script) @@ -312,6 +319,7 @@ class Scheduler(QObject): 'recipe':pt.name, 'title':recipe.get('title',''), 'urn':urn, + 'keep_issues':keep_issues } self.download_queue.add(urn) self.start_recipe_fetch.emit(arg) diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui index 8e6ab37162..079a17dbb3 100644 --- a/src/calibre/gui2/dialogs/scheduler.ui +++ b/src/calibre/gui2/dialogs/scheduler.ui @@ -245,6 +245,36 @@ + + + + + + Maximum number of copies (issues) of this recipe to keep. Set to 0 to keep all (disable). + + + Maximum copies (0 to keep all): + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index dce0b34aef..234d1fcfc4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1476,6 +1476,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ############# End get_categories + def get_most_recent_by_tag(self, number_to_keep, tag): + #Based on tag and number passed in, create a list of books matching that tag, keeping only the newest X versions + tag = tag.lower().strip() + idlist = [] + mycount = 0 + for myid in (self.conn.get('select a.book id from books_tags_link a inner join books b on a.book = b.id where a.tag in (select tags.id from tags where tags.name = ?) order by b.timestamp desc', [tag])): + myid = myid[0] + mycount = mycount + 1 + if mycount > int(number_to_keep): + idlist.append(myid) + return idlist + def tags_older_than(self, tag, delta): tag = tag.lower().strip() now = nowf() diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py index 5dd360213b..6697a1f39f 100644 --- a/src/calibre/web/feeds/recipes/collection.py +++ b/src/calibre/web/feeds/recipes/collection.py @@ -201,12 +201,14 @@ class SchedulerConfig(object): self.root.append(sr) self.write_scheduler_file() - def customize_recipe(self, urn, add_title_tag, custom_tags): + # 'keep_issues' argument for recipe-specific number of copies to keep + def customize_recipe(self, urn, add_title_tag, custom_tags, keep_issues): with self.lock: for x in list(self.iter_customization()): if x.get('id') == urn: self.root.remove(x) cs = E.recipe_customization({ + 'keep_issues' : keep_issues, 'id' : urn, 'add_title_tag' : 'yes' if add_title_tag else 'no', 'custom_tags' : ','.join(custom_tags), @@ -316,17 +318,20 @@ class SchedulerConfig(object): if x.get('id', False) == urn: return x.get('username', ''), x.get('password', '') + # 'keep_issues' element for recipe-specific number of copies to keep (default 0 == all) def get_customize_info(self, urn): + keep_issues = 0 add_title_tag = True custom_tags = [] with self.lock: for x in self.iter_customization(): if x.get('id', False) == urn: + keep_issues = x.get('keep_issues',0) add_title_tag = x.get('add_title_tag', 'yes') == 'yes' custom_tags = [i.strip() for i in x.get('custom_tags', '').split(',')] break - return add_title_tag, custom_tags + return add_title_tag, custom_tags, keep_issues def get_schedule_info(self, urn): with self.lock: diff --git a/src/calibre/web/feeds/recipes/model.py b/src/calibre/web/feeds/recipes/model.py index 559a5c08dd..203f96b03d 100644 --- a/src/calibre/web/feeds/recipes/model.py +++ b/src/calibre/web/feeds/recipes/model.py @@ -354,9 +354,10 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser): self.scheduler_config.schedule_recipe(self.recipe_from_urn(urn), sched_type, schedule) - def customize_recipe(self, urn, add_title_tag, custom_tags): + # 'keep_issues' argument for recipe-specific number of copies to keep + def customize_recipe(self, urn, add_title_tag, custom_tags, keep_issues): self.scheduler_config.customize_recipe(urn, add_title_tag, - custom_tags) + custom_tags, keep_issues) def get_to_be_downloaded_recipes(self): ans = self.scheduler_config.get_to_be_downloaded_recipes() From 7c22f4ffa145e8bb3cd927f475725a676eb3e74c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 26 Feb 2011 07:16:43 +0000 Subject: [PATCH 2/9] Fix #9169: not possible to hide user category with hierarchy Fix #9166: hierarchy indicator for no children Fix removes all hidden tag browser categories. They must be re-hidden. Fix adds clicking on category nodes to search, plus four-state searching. --- resources/images/minusminus.png | Bin 0 -> 1883 bytes resources/images/plusplus.png | Bin 0 -> 4249 bytes src/calibre/gui2/tag_view.py | 167 ++++++++++++++++++++------------ src/calibre/library/caches.py | 21 ++-- 4 files changed, 114 insertions(+), 74 deletions(-) create mode 100644 resources/images/minusminus.png create mode 100644 resources/images/plusplus.png diff --git a/resources/images/minusminus.png b/resources/images/minusminus.png new file mode 100644 index 0000000000000000000000000000000000000000..71225be8d7f6e5691e4efa5135c500c140141437 GIT binary patch literal 1883 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 F@*kvZ)O-K{ literal 0 HcmV?d00001 diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 2693ba8ed6..034d88e02d 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -21,6 +21,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE, gprefs from calibre.library.field_metadata import TagsIcons, category_icon_map +from calibre.library.database2 import Tag from calibre.utils.config import tweaks from calibre.utils.icu import sort_key, lower, strcmp from calibre.utils.search_query_parser import saved_searches @@ -69,7 +70,8 @@ class TagDelegate(QItemDelegate): # {{{ # }}} -TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2} +TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2, + 'mark_minus': 3, 'mark_minusminus': 4} class TagsView(QTreeView): # {{{ @@ -127,13 +129,17 @@ class TagsView(QTreeView): # {{{ self.set_new_model(self._model.get_filter_categories_by()) def set_database(self, db, tag_match, sort_by): - self.hidden_categories = db.prefs.get('tag_browser_hidden_categories', None) + hidden_cats = db.prefs.get('tag_browser_hidden_categories', None) + self.hidden_categories = [] # migrate from config to db prefs - if self.hidden_categories is None: - self.hidden_categories = config['tag_browser_hidden_categories'] - db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) - else: - self.hidden_categories = set(self.hidden_categories) + if hidden_cats is None: + hidden_cats = config['tag_browser_hidden_categories'] + # strip out any non-existence field keys + for cat in hidden_cats: + if cat in db.field_metadata: + self.hidden_categories.append(cat) + db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories)) + self.hidden_categories = set(self.hidden_categories) old = getattr(self, '_model', None) if old is not None: @@ -370,14 +376,15 @@ class TagsView(QTreeView): # {{{ action='delete_user_category', key=key)) self.context_menu.addSeparator() # Hide/Show/Restore categories - if not key.startswith('@') or key.find('.') < 0: - self.context_menu.addAction(_('Hide category %s') % category, - partial(self.context_menu_handler, action='hide', - category=category)) +# if not key.startswith('@') or key.find('.') < 0: + self.context_menu.addAction(_('Hide category %s') % category, + partial(self.context_menu_handler, action='hide', + category=key)) if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) - for col in sorted(self.hidden_categories, key=sort_key): - m.addAction(col, + for col in sorted(self.hidden_categories, + key=lambda x: sort_key(self.db.field_metadata[x]['name'])): + m.addAction(self.db.field_metadata[col]['name'], partial(self.context_menu_handler, action='show', category=col)) # search by category @@ -540,6 +547,7 @@ class TagTreeItem(object): # {{{ self.id_set = set() self.is_gst = False self.boxed = False + self.icon_state_map = list(map(QVariant, icon_map)) if self.parent is not None: self.parent.append(self) if data is None: @@ -554,9 +562,11 @@ class TagTreeItem(object): # {{{ self.bold_font = QVariant(self.bold_font) self.category_key = category_key self.temporary = temporary + self.tag = Tag(data) + self.tag.is_hierarchical = category_key.startswith('@') elif self.type == self.TAG: icon_map[0] = data.icon - self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) + self.tag = data if tooltip: self.tooltip = tooltip + ' ' else: @@ -593,6 +603,8 @@ class TagTreeItem(object): # {{{ if role == Qt.EditRole: return QVariant(self.py_name) if role == Qt.DecorationRole: + if self.tag.state: + return self.icon_state_map[self.tag.state] return self.icon if role == Qt.FontRole: return self.bold_font @@ -642,11 +654,22 @@ class TagTreeItem(object): # {{{ ''' set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES ''' - if self.type == self.TAG: - if set_to is None: - self.tag.state = (self.tag.state + 1)%3 - else: - self.tag.state = set_to +# if self.type == self.TAG: + if set_to is None: + while True: + self.tag.state = (self.tag.state + 1)%5 + if self.tag.state == TAG_SEARCH_STATES['mark_plus'] or \ + self.tag.state == TAG_SEARCH_STATES['mark_minus']: + if self.tag.is_editable: + break + elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\ + self.tag.state == TAG_SEARCH_STATES['mark_minusminus']: + if self.tag.is_hierarchical and len(self.children): + break + else: + break + else: + self.tag.state = set_to def child_tags(self): res = [] @@ -677,7 +700,8 @@ class TagsModel(QAbstractItemModel): # {{{ self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags'] self.drag_drop_finished = drag_drop_finished - self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('minus.png'))] + self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('plusplus.png')), + QIcon(I('minus.png')), QIcon(I('minusminus.png'))] self.db = db self.tags_view = parent self.hidden_categories = hidden_categories @@ -691,26 +715,33 @@ class TagsModel(QAbstractItemModel): # {{{ data = self.get_node_tree(config['sort_tags_by']) gst = db.prefs.get('grouped_search_terms', {}) - self.root_item = TagTreeItem() + self.root_item = TagTreeItem(icon_map=self.icon_state_map) self.category_nodes = [] last_category_node = None category_node_map = {} self.category_node_tree = {} - for i, r in enumerate(self.row_map): - if self.hidden_categories and self.categories[i] in self.hidden_categories: - continue + for i, key in enumerate(self.row_map): + if self.hidden_categories: + if key in self.hidden_categories: + continue + found = False + for cat in self.hidden_categories: + if cat.startswith('@') and key.startswith(cat + '.'): + found = True + if found: + continue is_gst = False - if r.startswith('@') and r[1:] in gst: - tt = _(u'The grouped search term name is "{0}"').format(r[1:]) + if key.startswith('@') and key[1:] in gst: + tt = _(u'The grouped search term name is "{0}"').format(key[1:]) is_gst = True - elif r == 'news': + elif key == 'news': tt = '' else: - tt = _(u'The lookup/search name is "{0}"').format(r) + tt = _(u'The lookup/search name is "{0}"').format(key) - if r.startswith('@'): - path_parts = [p for p in r.split('.')] + if key.startswith('@'): + path_parts = [p for p in key.split('.')] path = '' last_category_node = self.root_item tree_root = self.category_node_tree @@ -719,9 +750,10 @@ class TagsModel(QAbstractItemModel): # {{{ if path not in category_node_map: node = TagTreeItem(parent=last_category_node, data=p[1:] if i == 0 else p, - category_icon=self.category_icon_map[r], - tooltip=tt if path == r else path, - category_key=path) + category_icon=self.category_icon_map[key], + tooltip=tt if path == key else path, + category_key=path, + icon_map=self.icon_state_map) last_category_node = node category_node_map[path] = node self.category_nodes.append(node) @@ -736,11 +768,12 @@ class TagsModel(QAbstractItemModel): # {{{ path += '.' else: node = TagTreeItem(parent=self.root_item, - data=self.categories[i], - category_icon=self.category_icon_map[r], - tooltip=tt, category_key=r) + data=self.categories[key], + category_icon=self.category_icon_map[key], + tooltip=tt, category_key=key, + icon_map=self.icon_state_map) node.is_gst = False - category_node_map[r] = node + category_node_map[key] = node last_category_node = node self.category_nodes.append(node) self.refresh(data=data) @@ -1015,7 +1048,7 @@ class TagsModel(QAbstractItemModel): # {{{ def get_node_tree(self, sort): old_row_map = self.row_map[:] self.row_map = [] - self.categories = [] + self.categories = {} # Get the categories if self.search_restriction: @@ -1062,7 +1095,7 @@ class TagsModel(QAbstractItemModel): # {{{ for category in tb_categories: if category in data: # The search category can come and go self.row_map.append(category) - self.categories.append(tb_categories[category]['name']) + self.categories[category] = tb_categories[category]['name'] if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): # A category has been added or removed. We must force a rebuild of @@ -1163,7 +1196,8 @@ class TagsModel(QAbstractItemModel): # {{{ sub_cat = TagTreeItem(parent=category, data = name, tooltip = None, temporary=True, category_icon = category_node.icon, - category_key=category_node.category_key) + category_key=category_node.category_key, + icon_map=self.icon_state_map) self.endInsertRows() else: # by 'first letter' cl = cl_list[idx] @@ -1173,7 +1207,8 @@ class TagsModel(QAbstractItemModel): # {{{ data = collapse_letter, category_icon = category_node.icon, tooltip = None, temporary=True, - category_key=category_node.category_key) + category_key=category_node.category_key, + icon_map=self.icon_state_map) node_parent = sub_cat else: node_parent = category @@ -1477,16 +1512,16 @@ class TagsModel(QAbstractItemModel): # {{{ def reset_all_states(self, except_=None): update_list = [] def process_tag(tag_item): - if tag_item.type != TagTreeItem.CATEGORY: - tag = tag_item.tag - if tag is except_: - tag_index = self.createIndex(tag_item.row(), 0, tag_item) - self.dataChanged.emit(tag_index, tag_index) - elif tag.state != 0 or tag in update_list: - tag_index = self.createIndex(tag_item.row(), 0, tag_item) - tag.state = 0 - update_list.append(tag) - self.dataChanged.emit(tag_index, tag_index) +# if tag_item.type != TagTreeItem.CATEGORY: + tag = tag_item.tag + if tag is except_: + tag_index = self.createIndex(tag_item.row(), 0, tag_item) + self.dataChanged.emit(tag_index, tag_index) + elif tag.state != 0 or tag in update_list: + tag_index = self.createIndex(tag_item.row(), 0, tag_item) + tag.state = 0 + update_list.append(tag) + self.dataChanged.emit(tag_index, tag_index) for t in tag_item.children: process_tag(t) @@ -1503,13 +1538,11 @@ class TagsModel(QAbstractItemModel): # {{{ ''' if not index.isValid(): return False item = index.internalPointer() - if item.type == TagTreeItem.TAG: - item.toggle(set_to=set_to) - if exclusive: - self.reset_all_states(except_=item.tag) - self.dataChanged.emit(index, index) - return True - return False + item.toggle(set_to=set_to) + if exclusive: + self.reset_all_states(except_=item.tag) + self.dataChanged.emit(index, index) + return True def tokens(self): ans = [] @@ -1523,19 +1556,31 @@ class TagsModel(QAbstractItemModel): # {{{ # into the search string only once. The nodes_seen set helps us do that nodes_seen = set() + node_searches = {TAG_SEARCH_STATES['mark_plus'] : 'true', + TAG_SEARCH_STATES['mark_plusplus'] : '.true', + TAG_SEARCH_STATES['mark_minus'] : 'false', + TAG_SEARCH_STATES['mark_minusminus'] : '.false'} + for node in self.category_nodes: + if node.tag.state: + ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state])) + key = node.category_key for tag_item in node.child_tags(): tag = tag_item.tag if tag.state != TAG_SEARCH_STATES['clear']: - prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \ - else '' + if tag.state == TAG_SEARCH_STATES['mark_minus'] or \ + tag.state == TAG_SEARCH_STATES['mark_minusminus']: + prefix = ' not ' + else: + prefix = '' category = tag.category if key != 'news' else 'tag' if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating ans.append('%s%s:%s'%(prefix, category, len(tag.name))) else: name = original_name(tag) - use_prefix = tag.is_hierarchical + use_prefix = tag.state in [TAG_SEARCH_STATES['mark_plusplus'], + TAG_SEARCH_STATES['mark_minusminus']] if category == 'tags': if name in tags_seen: continue diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e626d446d2..0335c1d280 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -419,28 +419,23 @@ class ResultCache(SearchQueryParser): # {{{ def get_user_category_matches(self, location, query, candidates): res = set([]) - if self.db_prefs is None: + if self.db_prefs is None or len(query) < 2: return res user_cats = self.db_prefs.get('user_categories', []) c = set(candidates) - l = location.rfind('.') - if l > 0: - alt_loc = location[0:l] - alt_item = location[l+1:] + + if query.startswith('.'): + check_subcats = True + query = query[1:] else: - alt_loc = None + check_subcats = False + for key in user_cats: - if key == location or key.startswith(location + '.'): + if key == location or (check_subcats and key.startswith(location + '.')): for (item, category, ign) in user_cats[key]: s = self.get_matches(category, '=' + item, candidates=c) c -= s res |= s - elif key == alt_loc: - for (item, category, ign) in user_cats[key]: - if item == alt_item: - s = self.get_matches(category, '=' + item, candidates=c) - c -= s - res |= s if query == 'false': return candidates - res return res From 2aa275dad54555f8752069fd41cc4208e6975c87 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Feb 2011 11:43:10 +0000 Subject: [PATCH 3/9] 1) Fix problem with case sensitive matching when creating user categories 2) Fix problem in search where setting focus to the search box then removing it caused the item in history to replace the item in the search box case-insensitively. This broke tag state matching in the tag browser. --- src/calibre/gui2/dialogs/tag_categories.py | 6 ++++-- src/calibre/gui2/search_box.py | 4 ++++ src/calibre/gui2/tag_view.py | 13 +++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index 9bddb817cf..899d3d1920 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -178,8 +178,10 @@ class TagCategories(QDialog, Ui_TagCategories): 'multiple periods in a row or spaces before ' 'or after periods.')).exec_() return False - for c in self.categories: - if strcmp(c, cat_name) == 0: + for c in sorted(self.categories.keys(), key=sort_key): + if strcmp(c, cat_name) == 0 or \ + (icu_lower(cat_name).startswith(icu_lower(c) + '.') and\ + not cat_name.startswith(c + '.')): error_dialog(self, _('Name already used'), _('That name is already used, perhaps with different case.')).exec_() return False diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 34be6cd276..5a4c34a5cd 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -217,11 +217,15 @@ class SearchBox2(QComboBox): # {{{ self.clear() else: self.normalize_state() + self.lineEdit().setCompleter(None) self.setEditText(txt) self.line_edit.end(False) if emit_changed: self.changed.emit() self._do_search(store_in_history=store_in_history) + c = QCompleter() + self.lineEdit().setCompleter(c) + c.setCompletionMode(c.PopupCompletion) self.focus_to_library.emit() finally: if not store_in_history: diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 034d88e02d..5986717753 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -654,7 +654,6 @@ class TagTreeItem(object): # {{{ ''' set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES ''' -# if self.type == self.TAG: if set_to is None: while True: self.tag.state = (self.tag.state + 1)%5 @@ -1319,16 +1318,19 @@ class TagsModel(QAbstractItemModel): # {{{ return False user_cats = self.db.prefs.get('user_categories', {}) + user_cat_keys_lower = [icu_lower(k) for k in user_cats] ckey = item.category_key[1:] + ckey_lower = icu_lower(ckey) dotpos = ckey.rfind('.') if dotpos < 0: nkey = val else: nkey = ckey[:dotpos+1] + val - for c in user_cats: - if c.startswith(ckey): + nkey_lower = icu_lower(nkey) + for c in sorted(user_cats.keys(), key=sort_key): + if icu_lower(c).startswith(ckey_lower): if len(c) == len(ckey): - if nkey in user_cats: + if nkey_lower in user_cat_keys_lower: error_dialog(self.tags_view, _('Rename user category'), _('The name %s is already used')%nkey, show=True) return False @@ -1336,7 +1338,7 @@ class TagsModel(QAbstractItemModel): # {{{ del user_cats[ckey] elif c[len(ckey)] == '.': rest = c[len(ckey):] - if (nkey + rest) in user_cats: + if icu_lower(nkey + rest) in user_cat_keys_lower: error_dialog(self.tags_view, _('Rename user category'), _('The name %s is already used')%(nkey+rest), show=True) return False @@ -1512,7 +1514,6 @@ class TagsModel(QAbstractItemModel): # {{{ def reset_all_states(self, except_=None): update_list = [] def process_tag(tag_item): -# if tag_item.type != TagTreeItem.CATEGORY: tag = tag_item.tag if tag is except_: tag_index = self.createIndex(tag_item.row(), 0, tag_item) From 66197505871bb75d8b888aa10ccdcbeedb774bfd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Feb 2011 09:05:56 -0700 Subject: [PATCH 4/9] Dotpod by Federico Escalada --- resources/recipes/dotpod.recipe | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 resources/recipes/dotpod.recipe diff --git a/resources/recipes/dotpod.recipe b/resources/recipes/dotpod.recipe new file mode 100644 index 0000000000..b04945e6d4 --- /dev/null +++ b/resources/recipes/dotpod.recipe @@ -0,0 +1,27 @@ +__license__ = 'GPL v3' +__copyright__ = '2011-2011, Federico Escalada ' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Dotpod(BasicNewsRecipe): + __author__ = 'Federico Escalada' + description = 'Tecnologia y Comunicacion Audiovisual' + encoding = 'utf-8' + language = 'es' + max_articles_per_feed = 100 + no_stylesheets = True + oldest_article = 7 + publication_type = 'blog' + title = 'Dotpod' + authors = 'Federico Picone' + + conversion_options = { + 'authors' : authors + ,'comments' : description + ,'language' : language + } + + feeds = [('Dotpod', 'http://www.dotpod.com.ar/feed/')] + + remove_tags = [dict(name='div', attrs={'class':'feedflare'})] + From 72205310b6b455d9f8723b109d19f4dcbd584a42 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Feb 2011 09:07:50 -0700 Subject: [PATCH 5/9] Buffalo News by ChappyOnIce --- resources/recipes/buffalo_news.recipe | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 resources/recipes/buffalo_news.recipe diff --git a/resources/recipes/buffalo_news.recipe b/resources/recipes/buffalo_news.recipe new file mode 100644 index 0000000000..92c96757ae --- /dev/null +++ b/resources/recipes/buffalo_news.recipe @@ -0,0 +1,56 @@ +__license__ = 'GPL v3' +__author__ = 'Todd Chapman' +__copyright__ = 'Todd Chapman' +__version__ = 'v0.1' +__date__ = '26 February 2011' + +''' +http://www.buffalonews.com/RSS/ +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1298680852(BasicNewsRecipe): + title = u'Buffalo News' + __author__ = 'ChappyOnIce' + language = 'en' + oldest_article = 2 + max_articles_per_feed = 20 + encoding = 'utf-8' + remove_javascript = True + keep_only_tags = [ + dict(name='div', attrs={'class':['main-content-left']}) + ] + + remove_tags = [ + dict(name='div', attrs={'id':['commentCount']}), + dict(name='div', attrs={'class':['story-list-links']}) + ] + + remove_tags_after = dict(name='div', attrs={'class':['body storyContent']}) + conversion_options = { + 'base_font_size' : 14, + } + feeds = [(u'City of Buffalo', u'http://www.buffalonews.com/city/communities/buffalo/?widget=rssfeed&view=feed&contentId=77944'), + (u'Southern Erie County', u'http://www.buffalonews.com/city/communities/southern-erie/?widget=rssfeed&view=feed&contentId=77944'), + (u'Eastern Erie County', u'http://www.buffalonews.com/city/communities/eastern-erie/?widget=rssfeed&view=feed&contentId=77944'), + (u'Southern Tier', u'http://www.buffalonews.com/city/communities/southern-tier/?widget=rssfeed&view=feed&contentId=77944'), + (u'Niagara County', u'http://www.buffalonews.com/city/communities/niagara-county/?widget=rssfeed&view=feed&contentId=77944'), + (u'Business', u'http://www.buffalonews.com/business/?widget=rssfeed&view=feed&contentId=77944'), + (u'MoneySmart', u'http://www.buffalonews.com/business/moneysmart/?widget=rssfeed&view=feed&contentId=77944'), + (u'Bills & NFL', u'http://www.buffalonews.com/sports/bills-nfl/?widget=rssfeed&view=feed&contentId=77944'), + (u'Sabres & NHL', u'http://www.buffalonews.com/sports/sabres-nhl/?widget=rssfeed&view=feed&contentId=77944'), + (u'Bob DiCesare', u'http://www.buffalonews.com/sports/columns/bob-dicesare/?widget=rssfeed&view=feed&contentId=77944'), + (u'Bucky Gleason', u'http://www.buffalonews.com/sports/columns/bucky-gleason/?widget=rssfeed&view=feed&contentId=77944'), + (u'Mark Gaughan', u'http://www.buffalonews.com/sports/bills-nfl/inside-the-nfl/?widget=rssfeed&view=feed&contentId=77944'), + (u'Mike Harrington', u'http://www.buffalonews.com/sports/columns/mike-harrington/?widget=rssfeed&view=feed&contentId=77944'), + (u'Jerry Sullivan', u'http://www.buffalonews.com/sports/columns/jerry-sullivan/?widget=rssfeed&view=feed&contentId=77944'), + (u'Other Sports Columns', u'http://www.buffalonews.com/sports/columns/other-sports-columns/?widget=rssfeed&view=feed&contentId=77944'), + (u'Life', u'http://www.buffalonews.com/life/?widget=rssfeed&view=feed&contentId=77944'), + (u'Bruce Andriatch', u'http://www.buffalonews.com/city/columns/bruce-andriatch/?widget=rssfeed&view=feed&contentId=77944'), + (u'Donn Esmonde', u'http://www.buffalonews.com/city/columns/donn-esmonde/?widget=rssfeed&view=feed&contentId=77944'), + (u'Rod Watson', u'http://www.buffalonews.com/city/columns/rod-watson/?widget=rssfeed&view=feed&contentId=77944'), + (u'Entertainment', u'http://www.buffalonews.com/entertainment/?widget=rssfeed&view=feed&contentId=77944'), + (u'Off Main Street', u'http://www.buffalonews.com/city/columns/off-main-street/?widget=rssfeed&view=feed&contentId=77944'), + (u'Editorials', u'http://www.buffalonews.com/editorial-page/buffalo-news-editorials/?widget=rssfeed&view=feed&contentId=77944') + ] From dab97fa3f1d4af159eb9e302e0642c27268e5415 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Feb 2011 17:19:14 +0000 Subject: [PATCH 6/9] Update gui.rst to reflect 5-state searching --- src/calibre/manual/gui.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index 882e3d5921..210bd0569e 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -436,25 +436,26 @@ Tag Browser .. image:: images/tag_browser.png :class: float-left-img -The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any Item in the Tag Browser, for example, the Author name, Isaac Asimov, then the list of books to the right is restricted to books by that author. Clicking once again on Isaac Asimov will restrict the list of books to books not by Isaac Asimov. A third click will remove the restriction. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could Hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. It is a good way to learn how to construct basic search expressions. +The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any item in the Tag Browser, for example the author name Isaac Asimov, then the list of books to the right is restricted to showing books by that author. You can click on category names as well. For example, clicking on "Series" will show you all books in any series. -There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose to hide it or rename it or open a "Manage x" dialog that allows you to manage items of that kind. For example the "Manage Authors" dialog allows you to rename authors and control how their names are sorted. +The first click on an item will restrict the list of books to those that contain/match the item. Continuing the above example, clicking on Isaac Asimov will show books by that author. Clicking again on the item will change what is shown, depending on whether the item has children (see sub-categories and hierarchical items below). Continuing the Isaac Asimov example, clicking again on Isaac Asimov will restrict the list of books to those not by Isaac Asimov. A third click will remove the restriction, showing all books. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. Looking at what the Tag Browser generates is a good way to learn how to construct basic search expressions. Items in the Tag browser have their icons partially colored. The amount of color depends on the average rating of the books in that category. So for example if the books by Isaac Asimov have an average of four stars, the icon for Isaac Asimov in the Tag Browser will be 4/5th colored. You can hover your mouse over the icon to see the average rating. -For convenience, you can drag and drop books from the book list to items in the Tag Browser and that item will be automatically applied to the dropped books. For example, dragging a book to Isaac Asimov will set the author of that book to Isaac Asimov or dragging it to the tag History will add the tag History to its tags. +The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the User Categories Editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the sub-category name; or by using the User Categories Editor by entering names like the Favorites example above. -The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the user categories editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories act like built-in categories; you can click on items to search for them. You can search for all items in a category by right-clicking on the category name and choosing "Search for books in ...". +You can search user categories in the same way as built-in categories, by clicking on them. There are four different searches cycled through by clicking: "everything matching an item in the category" indicated by a single green plus sign, "everything matching an item in the category or its sub-categories" indicated by two green plus signs, "everything not matching an item in the category" shown by a single red minus sign, and "everything not matching an item in the category or its sub-categories" shown by two red minus signs. -User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created using Manage User Categories by entering names like the Favorites example. They can also be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the category name. +It is also possible to create hierarchies inside some of the text categories such as tags, series, and custom columns. These hierarchies show with the small triangle, permitting the sub-items to be hidden. To use hierarchies of items in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items. -It is also possible to create hierarchies inside some of the built-in categories (the text categories). These hierarchies show with the small triangle permitting the sub-items to be hidden. To use hierarchies in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items. +Hierarchical items (items with children) use the same four 'click-on' searches as user categories. Items that do not have children use two of the searches: "everything matching" and "everything not matching". -You can drag and drop items in the Tag browser onto user categories to add them to that category. +You can drag and drop items in the Tag browser onto user categories to add them to that category. If the source is a user category, holding the shift key while dragging will move the item to the new category. You can also drag and drop books from the book list onto items in the Tag Browser; dropping a book on an item causes that item to be automatically applied to the dropped books. For example, dragging a book onto Isaac Asimov will set the author of that book to Isaac Asimov. Dropping it onto the tag History will add the tag History to the book's tags. + +There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose one of several operations. Some examples are to hide the it, rename it, or open a "Manage x" dialog that allows you to manage items of that kind. For example, the "Manage Authors" dialog allows you to rename authors and control how their names are sorted. You can control how items are sorted in the Tag browser via the box at the bottom of the Tag Browser. You can choose to sort by name, average rating or popularity (popularity is the number of books with an item in your library; for example; the popularity of Isaac Asimov is the number of book sin your library by Isaac Asimov). - Jobs ----- .. image:: images/jobs.png From b8f08346b52994da795a9865bf2d8697496a489d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Feb 2011 10:33:24 -0700 Subject: [PATCH 7/9] Historia and Buctaras by Silviu Coatara --- resources/images/news/bucataras.png | Bin 0 -> 765 bytes resources/images/news/historiaro.png | Bin 0 -> 521 bytes resources/recipes/bucataras.recipe | 56 +++++++++++++++++++++++++++ resources/recipes/historiaro.recipe | 51 ++++++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 resources/images/news/bucataras.png create mode 100644 resources/images/news/historiaro.png create mode 100644 resources/recipes/bucataras.recipe create mode 100644 resources/recipes/historiaro.recipe diff --git a/resources/images/news/bucataras.png b/resources/images/news/bucataras.png new file mode 100644 index 0000000000000000000000000000000000000000..fae90e17c4366b5accc5668256f5234effee9f84 GIT binary patch literal 765 zcmVI>y9+ zP1{Jz+)n3m&Ug8_Xo5WZ_j&Wa>igYY0YCr+q99R*%)r%!CPmgHc!yTt1O{`@(0QgR z;sh*jP#~a0u!$ij(kTj}7sLbvMFY)pu4z|-i-sX!B?@n&0f~|GttPFkL7hPG3C;(c zuW3gA{qlK5b8N~?XYk9B@nd18qHz` zMVjKOIX*_}x<*MPh@%dkne&vj!=c{(0IX5uMssC(nY@|f>WUCNK_aoPsC?j+a|{9q z31F}yf{1`pq$n0pbf5i6IiIk4@iLcJmdMivqsf$hIbonPY5^B&B621;n-EEi@bced zetWph%TdX+yvO?c7x?nd9d4|x;DaDK;$p-T(SQ~aBu1iANHEyhoQ<`0a$~suZoyaA zuF(B`haZ0a6{8{524fU(meUd=N@*IYWmZmkynD#EkG2?`PI$6k((PA#a^q9(-h0S{ zpSCE_w5%n!381k7Ax$iW7uM#-tiNN}?^PTPhlozN-`(TUUp*EUVRPpX-pey?ef$x} z?ShzP*jC=eD8s5KK4>gKTi7^%j#qv{r(tMin$hSr%}gjm}%sH?W`G{v4Aec$2X5;dPG3-*I6! zq1nDjQ8dYmMV|NmhIu5>2~m57qX7>dKjUcBN8P>uja1R`dU(QWa*lRpnL5j4Qqjs6 vXyhIGr(-rZpY!72fU#4gse;5K^_>3zn;>=X(V!n600000NkvXXu0mjfoBnR* literal 0 HcmV?d00001 diff --git a/resources/images/news/historiaro.png b/resources/images/news/historiaro.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e616c8761974bdafca3acb1f3302a1ea8b5238 GIT binary patch literal 521 zcmV+k0`~ohP)O3DDx0nRYNXo8)H%u0d@zJVk*42e%coZt(nZ=p27N$UtlEeV$y zGU$Y+QYO75J=eXQ&{JD`T$8=`%G%$be_yP@V4!NH!jEc|PNzc{1gsVcY;L}0`N=W> z^;(VZhlgDLy2P?AmX;n<+$i#DeH|ppbzObj-xr`q^9yP=n=-~|s4xt5Tt5~dK;?t7 zf*_D2F$|raoCwg)yONT$`s}&#GdcBoz1Xw4mFv0y_`Xk)jD&65c%Bz~F+jtRF^K?f z0gmnb3mD3>tYm;SF-`+Gjx%YE!nT8?F)#HuJahLRXQyYp-`&M?JtPBIq*5uODB}A1 zH~D;iIzWIQ1}r{YU7`8@S|h zGb7XYeVw15Yo}BapxOHmbnx|?TCG<6-W!dEzI^%|f2I$6d%C!|kTFJ9x7$^@T&8(- z#aw=lUblzqxfC~wyeJd^s8%Y}Yc*zav!v50ZrV4jJY8XH>kWSZgq{$j2O3Ch00000 LNkvXXu0mjfVkGfv literal 0 HcmV?d00001 diff --git a/resources/recipes/bucataras.recipe b/resources/recipes/bucataras.recipe new file mode 100644 index 0000000000..b069ecc5b0 --- /dev/null +++ b/resources/recipes/bucataras.recipe @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2011, Silviu Cotoar\u0103' +''' +bucataras.ro +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Bucataras(BasicNewsRecipe): + title = u'Bucataras' + __author__ = u'Silviu Cotoar\u0103' + description = '' + publisher = 'Bucataras' + oldest_article = 5 + language = 'ro' + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + category = 'Ziare,Bucatarie,Retete' + encoding = 'utf-8' + cover_url = 'http://www.bucataras.ro/templates/default/images/pink/logo.jpg' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [ + dict(name='h1', attrs={'class':'titlu'}) + , dict(name='div', attrs={'class':'contentL'}) + , dict(name='div', attrs={'class':'contentBottom'}) + + ] + + remove_tags = [ + dict(name='div', attrs={'class':['sociale']}) + , dict(name='div', attrs={'class':['contentR']}) + , dict(name='a', attrs={'target':['_self']}) + , dict(name='div', attrs={'class':['comentarii']}) + ] + + remove_tags_after = [ + dict(name='div', attrs={'class':['comentarii']}) + ] + + feeds = [ + (u'Feeds', u'http://www.bucataras.ro/rss/retete/') + ] + + def preprocess_html(self, soup): + return self.adeify_images(soup) diff --git a/resources/recipes/historiaro.recipe b/resources/recipes/historiaro.recipe new file mode 100644 index 0000000000..98eb5b6dfe --- /dev/null +++ b/resources/recipes/historiaro.recipe @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2011, Silviu Cotoar\u0103' +''' +historia.ro +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class HistoriaRo(BasicNewsRecipe): + title = u'Historia' + __author__ = u'Silviu Cotoar\u0103' + description = '' + publisher = 'Historia' + oldest_article = 5 + language = 'ro' + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + category = 'Ziare,Reviste,Istorie' + encoding = 'utf-8' + cover_url = 'http://www.historia.ro/sites/all/themes/historia/images/historia.png' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [ + dict(name='div', attrs={'class':'c_antet_title'}) + , dict(name='a', attrs={'class':'overlaybox'}) + , dict(name='div', attrs={'class':'art_content'}) + ] + + remove_tags = [ + dict(name='div', attrs={'class':['fl_left']}) + , dict(name='div', attrs={'id':['article_toolbar']}) + , dict(name='div', attrs={'class':['zoom_cont']}) + ] + + + feeds = [ + (u'Feeds', u'http://www.historia.ro/rss.xml') + ] + + def preprocess_html(self, soup): + return self.adeify_images(soup) From 4fcb4fc42e125ab09e76114d215bab2d4c76affe Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Feb 2011 20:36:45 +0000 Subject: [PATCH 8/9] Ignore errors when setting preferences in clean_user_categories --- src/calibre/library/database2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bf3e9c8a14..4d6681b91d 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1195,7 +1195,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): i += 1 else: new_cats['.'.join(comps)] = user_cats[k] - self.prefs.set('user_categories', new_cats) + try: + self.prefs.set('user_categories', new_cats) + except: + pass return new_cats def get_categories(self, sort='name', ids=None, icon_map=None): From e16f04e7b7fbd3b99826f1495d7002c2826bf79c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Feb 2011 16:18:56 -0700 Subject: [PATCH 9/9] When keeping x periodical issues be a little more careful about what we delete --- src/calibre/gui2/actions/fetch_news.py | 3 ++- src/calibre/library/database2.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index fe51012e31..f7756efbab 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -67,7 +67,8 @@ class FetchNewsAction(InterfaceAction): keep_issues = 0 if keep_issues > 0: ids_with_tag = list(sorted(self.gui.library_view.model(). - db.tags_older_than(arg['title'], None), reverse=True)) + db.tags_older_than(arg['title'], + None, must_have_tag=_('News')), reverse=True)) ids_to_delete = ids_with_tag[keep_issues:] if ids_to_delete: self.gui.library_view.model().delete_books_by_id(ids_to_delete) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 4d6681b91d..8e90fe77bd 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1503,25 +1503,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ############# End get_categories - def tags_older_than(self, tag, delta): + def tags_older_than(self, tag, delta, must_have_tag=None): ''' Return the ids of all books having the tag ``tag`` that are older than than the specified time. tag comparison is case insensitive. :param delta: A timedelta object or None. If None, then all ids with the tag are returned. + :param must_have_tag: If not None the list of matches will be + restricted to books that have this tag ''' tag = tag.lower().strip() + mht = must_have_tag.lower().strip() if must_have_tag else None now = nowf() tindex = self.FIELD_MAP['timestamp'] gindex = self.FIELD_MAP['tags'] + iindex = self.FIELD_MAP['id'] for r in self.data._data: if r is not None: if delta is None or (now - r[tindex]) > delta: tags = r[gindex] - if tags and tag in [x.strip() for x in - tags.lower().split(',')]: - yield r[self.FIELD_MAP['id']] + if tags: + tags = [x.strip() for x in tags.lower().split(',')] + if tag in tags and (mht is None or mht in tags): + yield r[iindex] def get_next_series_num_for(self, series): series_id = self.conn.get('SELECT id from series WHERE name=?',