mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Allow searching by the number of tags/authors/formats/etc. See User Manual for details. Fix #7459 (Bug/error when copying and deleting from one library to another). Fix #7548
This commit is contained in:
commit
c5204ef656
@ -32,15 +32,15 @@ class NewYorker(BasicNewsRecipe):
|
|||||||
, 'publisher' : publisher
|
, 'publisher' : publisher
|
||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'class':'headers'})
|
dict(name='div', attrs={'class':'headers'})
|
||||||
,dict(name='div', attrs={'id':['articleheads','items-container','articleRail','articletext','photocredits']})
|
,dict(name='div', attrs={'id':['articleheads','items-container','articleRail','articletext','photocredits']})
|
||||||
]
|
]
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name=['meta','iframe','base','link','embed','object'])
|
dict(name=['meta','iframe','base','link','embed','object'])
|
||||||
,dict(attrs={'class':['utils','articleRailLinks','icons'] })
|
,dict(attrs={'class':['utils','articleRailLinks','icons'] })
|
||||||
,dict(attrs={'id':['show-header','show-footer'] })
|
,dict(attrs={'id':['show-header','show-footer'] })
|
||||||
]
|
]
|
||||||
remove_attributes = ['lang']
|
remove_attributes = ['lang']
|
||||||
feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')]
|
feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')]
|
||||||
@ -58,4 +58,4 @@ class NewYorker(BasicNewsRecipe):
|
|||||||
if cover_item:
|
if cover_item:
|
||||||
cover_url = 'http://www.newyorker.com' + cover_item['src'].strip()
|
cover_url = 'http://www.newyorker.com' + cover_item['src'].strip()
|
||||||
return cover_url
|
return cover_url
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class RusiaHoy(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'es'
|
language = 'es'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
extra_css = """
|
extra_css = """
|
||||||
body{font-family: Arial,sans-serif }
|
body{font-family: Arial,sans-serif }
|
||||||
.article_article_title{font-size: xx-large; font-weight: bold}
|
.article_article_title{font-size: xx-large; font-weight: bold}
|
||||||
.article_date{color: black; font-size: small}
|
.article_date{color: black; font-size: small}
|
||||||
@ -44,4 +44,4 @@ class RusiaHoy(BasicNewsRecipe):
|
|||||||
for item in soup.findAll(style=True):
|
for item in soup.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
@ -437,7 +437,7 @@ class BulkBool(BulkBase, Bool):
|
|||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
val = False
|
val = False
|
||||||
if value is not None and value != val:
|
if value is not None and value != val:
|
||||||
return None
|
return 'nochange'
|
||||||
value = val
|
value = val
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -445,19 +445,23 @@ class BulkBool(BulkBase, Bool):
|
|||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||||
QComboBox(parent)]
|
QComboBox(parent)]
|
||||||
w = self.widgets[1]
|
w = self.widgets[1]
|
||||||
items = [_('Yes'), _('No'), _('Undefined')]
|
items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')]
|
||||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')]
|
||||||
for icon, text in zip(icons, items):
|
for icon, text in zip(icons, items):
|
||||||
w.addItem(QIcon(icon), text)
|
w.addItem(QIcon(icon), text)
|
||||||
|
|
||||||
|
def getter(self):
|
||||||
|
val = self.widgets[1].currentIndex()
|
||||||
|
return {3: 'nochange', 2: None, 1: False, 0: True}[val]
|
||||||
|
|
||||||
def setter(self, val):
|
def setter(self, val):
|
||||||
val = {None: 2, False: 1, True: 0}[val]
|
val = {'nochange': 3, None: 2, False: 1, True: 0}[val]
|
||||||
self.widgets[1].setCurrentIndex(val)
|
self.widgets[1].setCurrentIndex(val)
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
def commit(self, book_ids, notify=False):
|
||||||
val = self.gui_val
|
val = self.gui_val
|
||||||
val = self.normalize_ui_val(val)
|
val = self.normalize_ui_val(val)
|
||||||
if val != self.initial_val:
|
if val != self.initial_val and val != 'nochange':
|
||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
val = False
|
val = False
|
||||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||||
|
@ -403,7 +403,7 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
'<=':[2, lambda r, q: r <= q]
|
'<=':[2, lambda r, q: r <= q]
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_numeric_matches(self, location, query):
|
def get_numeric_matches(self, location, query, val_func = None):
|
||||||
matches = set([])
|
matches = set([])
|
||||||
if len(query) == 0:
|
if len(query) == 0:
|
||||||
return matches
|
return matches
|
||||||
@ -419,7 +419,10 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
if relop is None:
|
if relop is None:
|
||||||
(p, relop) = self.numeric_search_relops['=']
|
(p, relop) = self.numeric_search_relops['=']
|
||||||
|
|
||||||
loc = self.field_metadata[location]['rec_index']
|
if val_func is None:
|
||||||
|
loc = self.field_metadata[location]['rec_index']
|
||||||
|
val_func = lambda item, loc=loc: item[loc]
|
||||||
|
|
||||||
dt = self.field_metadata[location]['datatype']
|
dt = self.field_metadata[location]['datatype']
|
||||||
if dt == 'int':
|
if dt == 'int':
|
||||||
cast = (lambda x: int (x))
|
cast = (lambda x: int (x))
|
||||||
@ -430,6 +433,9 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
elif dt == 'float':
|
elif dt == 'float':
|
||||||
cast = lambda x : float (x)
|
cast = lambda x : float (x)
|
||||||
adjust = lambda x: x
|
adjust = lambda x: x
|
||||||
|
else: # count operation
|
||||||
|
cast = (lambda x: int (x))
|
||||||
|
adjust = lambda x: x
|
||||||
|
|
||||||
if len(query) > 1:
|
if len(query) > 1:
|
||||||
mult = query[-1:].lower()
|
mult = query[-1:].lower()
|
||||||
@ -446,10 +452,11 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
for item in self._data:
|
for item in self._data:
|
||||||
if item is None:
|
if item is None:
|
||||||
continue
|
continue
|
||||||
if not item[loc]:
|
v = val_func(item)
|
||||||
|
if not v:
|
||||||
i = 0
|
i = 0
|
||||||
else:
|
else:
|
||||||
i = adjust(item[loc])
|
i = adjust(v)
|
||||||
if relop(i, q):
|
if relop(i, q):
|
||||||
matches.add(item[0])
|
matches.add(item[0])
|
||||||
return matches
|
return matches
|
||||||
@ -467,15 +474,23 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
return matches
|
return matches
|
||||||
raise ParseException(query, len(query), 'Recursive query group detected', self)
|
raise ParseException(query, len(query), 'Recursive query group detected', self)
|
||||||
|
|
||||||
# take care of dates special case
|
if location in self.field_metadata:
|
||||||
if location in self.field_metadata and \
|
fm = self.field_metadata[location]
|
||||||
self.field_metadata[location]['datatype'] == 'datetime':
|
# take care of dates special case
|
||||||
return self.get_dates_matches(location, query.lower())
|
if fm['datatype'] == 'datetime':
|
||||||
|
return self.get_dates_matches(location, query.lower())
|
||||||
|
|
||||||
# take care of numbers special case
|
# take care of numbers special case
|
||||||
if location in self.field_metadata and \
|
if fm['datatype'] in ('rating', 'int', 'float'):
|
||||||
self.field_metadata[location]['datatype'] in ('rating', 'int', 'float'):
|
return self.get_numeric_matches(location, query.lower())
|
||||||
return self.get_numeric_matches(location, query.lower())
|
|
||||||
|
# take care of the 'count' operator for is_multiples
|
||||||
|
if fm['is_multiple'] and \
|
||||||
|
len(query) > 1 and query.startswith('#') and \
|
||||||
|
query[1:1] in '=<>!':
|
||||||
|
vf = lambda item, loc=fm['rec_index'], ms=fm['is_multiple']:\
|
||||||
|
len(item[loc].split(ms)) if item[loc] is not None else 0
|
||||||
|
return self.get_numeric_matches(location, query[1:], val_func=vf)
|
||||||
|
|
||||||
# everything else, or 'all' matches
|
# everything else, or 'all' matches
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = CONTAINS_MATCH
|
||||||
|
@ -268,8 +268,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
base,
|
base,
|
||||||
prefer_custom=True)
|
prefer_custom=True)
|
||||||
|
|
||||||
self.field_metadata.set_field_record_index('cover',
|
|
||||||
self.FIELD_MAP['cover'], prefer_custom=False)
|
|
||||||
self.FIELD_MAP['ondevice'] = base+1
|
self.FIELD_MAP['ondevice'] = base+1
|
||||||
self.field_metadata.set_field_record_index('ondevice', base+1, prefer_custom=False)
|
self.field_metadata.set_field_record_index('ondevice', base+1, prefer_custom=False)
|
||||||
self.FIELD_MAP['all_metadata'] = base+2
|
self.FIELD_MAP['all_metadata'] = base+2
|
||||||
|
@ -3,6 +3,7 @@ Created on 25 May 2010
|
|||||||
|
|
||||||
@author: charles
|
@author: charles
|
||||||
'''
|
'''
|
||||||
|
import copy
|
||||||
|
|
||||||
from calibre.utils.ordered_dict import OrderedDict
|
from calibre.utils.ordered_dict import OrderedDict
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
@ -86,7 +87,7 @@ class FieldMetadata(dict):
|
|||||||
|
|
||||||
# Builtin metadata {{{
|
# Builtin metadata {{{
|
||||||
|
|
||||||
_field_metadata = [
|
_field_metadata_prototype = [
|
||||||
('authors', {'table':'authors',
|
('authors', {'table':'authors',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column':'author',
|
'link_column':'author',
|
||||||
@ -161,6 +162,15 @@ class FieldMetadata(dict):
|
|||||||
'search_terms':['tags', 'tag'],
|
'search_terms':['tags', 'tag'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':True}),
|
'is_category':True}),
|
||||||
|
('all_metadata',{'table':None,
|
||||||
|
'column':None,
|
||||||
|
'datatype':None,
|
||||||
|
'is_multiple':None,
|
||||||
|
'kind':'field',
|
||||||
|
'name':None,
|
||||||
|
'search_terms':[],
|
||||||
|
'is_custom':False,
|
||||||
|
'is_category':False}),
|
||||||
('author_sort',{'table':None,
|
('author_sort',{'table':None,
|
||||||
'column':None,
|
'column':None,
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
@ -180,7 +190,7 @@ class FieldMetadata(dict):
|
|||||||
'is_custom':False, 'is_category':False}),
|
'is_custom':False, 'is_category':False}),
|
||||||
('cover', {'table':None,
|
('cover', {'table':None,
|
||||||
'column':None,
|
'column':None,
|
||||||
'datatype':None,
|
'datatype':'int',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':None,
|
||||||
@ -223,15 +233,6 @@ class FieldMetadata(dict):
|
|||||||
'search_terms':[],
|
'search_terms':[],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False}),
|
'is_category':False}),
|
||||||
('all_metadata',{'table':None,
|
|
||||||
'column':None,
|
|
||||||
'datatype':None,
|
|
||||||
'is_multiple':None,
|
|
||||||
'kind':'field',
|
|
||||||
'name':None,
|
|
||||||
'search_terms':[],
|
|
||||||
'is_custom':False,
|
|
||||||
'is_category':False}),
|
|
||||||
('ondevice', {'table':None,
|
('ondevice', {'table':None,
|
||||||
'column':None,
|
'column':None,
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
@ -322,6 +323,7 @@ class FieldMetadata(dict):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._field_metadata = copy.deepcopy(self._field_metadata_prototype)
|
||||||
self._tb_cats = OrderedDict()
|
self._tb_cats = OrderedDict()
|
||||||
self._search_term_map = {}
|
self._search_term_map = {}
|
||||||
self.custom_label_to_key_map = {}
|
self.custom_label_to_key_map = {}
|
||||||
|
@ -274,6 +274,14 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col
|
|||||||
|
|
||||||
:guilabel:`Advanced Search Dialog`
|
:guilabel:`Advanced Search Dialog`
|
||||||
|
|
||||||
|
You can test for the number of items in multiple-value columns, such as tags, formats, authors, and tags-like custom columns. This is done using a syntax very similar to numeric tests (discussed above), except that the relational operator begins with a ``#`` character. For example::
|
||||||
|
|
||||||
|
tags:#>3 will give you books with more than three tags
|
||||||
|
tags:#!=3 will give you books that do not have three tags
|
||||||
|
authors:#=1 will give you books with exactly one author
|
||||||
|
#cust:#<5 will give you books with less than five items in custom column #cust
|
||||||
|
formats:#>1 will give you books with more than one format
|
||||||
|
|
||||||
Saving searches
|
Saving searches
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user