merge from trunk

This commit is contained in:
Lee 2011-04-27 23:56:06 +08:00
commit 052b0044e2
40 changed files with 440 additions and 316 deletions

View File

@ -48,7 +48,7 @@ class LeMonde(BasicNewsRecipe):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
return self.adeify_images(soup)
preprocess_regexps = [
(re.compile(r'([0-9])%'), lambda m: m.group(1) + ' %'),

View File

@ -118,6 +118,7 @@ sort_columns_at_startup = None
# timestamp default if not set: dd MMM yyyy
gui_pubdate_display_format = 'MMM yyyy'
gui_timestamp_display_format = 'dd MMM yyyy'
gui_last_modified_display_format = 'dd MMM yyyy'
#: Control sorting of titles and series in the library display
# Control title and series sorting in the library view. If set to

View File

@ -69,7 +69,24 @@ nmake -f ms\ntdll.mak install
Qt
--------
Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make::
Extract Qt sourcecode to C:\Qt\4.x.x.
Qt uses its own routine to locate and load "system libraries" including the openssl libraries needed for "Get Books". This means that we have to apply the following patch to have Qt load the openssl libraries bundled with calibre:
--- src/corelib/plugin/qsystemlibrary.cpp 2011-02-22 05:04:00.000000000 -0700
+++ src/corelib/plugin/qsystemlibrary.cpp 2011-04-25 20:53:13.635247466 -0600
@@ -110,7 +110,7 @@ HINSTANCE QSystemLibrary::load(const wch
#if !defined(QT_BOOTSTRAPPED)
if (!onlySystemDirectory)
- searchOrder << QFileInfo(qAppFileName()).path();
+ searchOrder << (QFileInfo(qAppFileName()).path().replace(QLatin1Char('/'), QLatin1Char('\\')) + QString::fromLatin1("\\DLLs\\"));
#endif
searchOrder << qSystemDirectory();
Now, run configure and make::
configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake

View File

@ -11,7 +11,10 @@
SummaryCodepage='1252' />
<Media Id="1" Cabinet="{app}.cab" CompressionLevel="{compression}" EmbedCab="yes" />
<!-- The following line is needed because of the patch to QtCore4.dll. You can remove this line
after you update Qt beyond 4.7.2. 'emus' means re-install even if version is the same not just if it is older. -->
<Property Id='REINSTALLMODE' Value='emus'/>
<Upgrade Id="{upgrade_code}">
<UpgradeVersion Maximum="{version}"
IncludeMaximum="yes"

View File

@ -347,9 +347,10 @@ class UploadUserManual(Command): # {{{
with NamedTemporaryFile(suffix='.zip') as f:
os.fchmod(f.fileno(),
stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE)
with CurrentDir(self.d(path)):
with CurrentDir(path):
with ZipFile(f, 'w') as zf:
for x in os.listdir('.'):
if x.endswith('.swp'): continue
zf.write(x)
if os.path.isdir(x):
for y in os.listdir(x):

View File

@ -388,7 +388,11 @@ class CurrentDir(object):
return self.cwd
def __exit__(self, *args):
os.chdir(self.cwd)
try:
os.chdir(self.cwd)
except:
# The previous CWD no longer exists
pass
class StreamReadWrapper(object):

View File

@ -460,7 +460,7 @@ class ITUNES(DriverBase):
cached_books[this_book.path] = {
'title':book.Name,
'author':book.artist().split(' & '),
'author':book.Artist.split(' & '),
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
'uuid': book.Composer,
'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub'

View File

@ -402,7 +402,7 @@ class HTMLPreProcessor(object):
(re.compile(r'((?<=</a>)\s*file:/{2,4}[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
# Center separator lines
(re.compile(u'<br>\s*(?P<break>([*#•✦=]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),
(re.compile(u'<br>\s*(?P<break>([*#•✦=] *){3,})\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group('break') + '</p>'),
# Remove page links
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),

View File

@ -120,7 +120,11 @@ class Metadata(object):
_('TEMPLATE ERROR'),
self).strip()
return val
if field.startswith('#') and field.endswith('_index'):
try:
return self.get_extra(field[:-6])
except:
pass
raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))
@ -170,11 +174,6 @@ class Metadata(object):
try:
return self.__getattribute__(field)
except AttributeError:
if field.startswith('#') and field.endswith('_index'):
try:
return self.get_extra(field[:-6])
except:
pass
return default
def get_extra(self, field, default=None):
@ -631,7 +630,7 @@ class Metadata(object):
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'rating':
res = res/2.0
elif key in ('book_size', 'size'):
elif key == 'size':
res = human_readable(res)
return (name, unicode(res), orig_res, fmeta)

View File

@ -400,7 +400,8 @@ class MetadataUpdater(object):
if getattr(self, 'exth', None) is None:
raise MobiError('No existing EXTH record. Cannot update metadata.')
self.record0[92:96] = iana2mobi(mi.language)
if not mi.is_null('language'):
self.record0[92:96] = iana2mobi(mi.language)
self.create_exth(exth=exth, new_title=mi.title)
# Fetch updated timestamp, cover_record, thumbnail_record

View File

@ -301,7 +301,7 @@ class Amazon(Source):
if asin is None:
asin = identifiers.get('asin', None)
if asin:
return 'http://amzn.com/%s'%asin
return ('amazon', asin, 'http://amzn.com/%s'%asin)
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{

View File

@ -56,7 +56,8 @@ class InternalMetadataCompareKeyGen(object):
'''
Generate a sort key for comparison of the relevance of Metadata objects,
given a search query.
given a search query. This is used only to compare results from the same
metadata source, not across different sources.
The sort key ensures that an ascending order sort is a sort by order of
decreasing relevance.
@ -374,7 +375,11 @@ class Source(Plugin):
def get_book_url(self, identifiers):
'''
Return the URL for the book identified by identifiers at this source.
Return a 3-tuple or None. The 3-tuple is of the form:
(identifier_type, identifier_value, URL).
The URL is the URL for the book identified by identifiers at this
source. identifier_type, identifier_value specify the identifier
corresponding to the URL.
This URL must be browseable to by a human using a browser. It is meant
to provide a clickable link for the user to easily visit the books page
at this source.

View File

@ -173,7 +173,7 @@ class GoogleBooks(Source):
def get_book_url(self, identifiers): # {{{
goog = identifiers.get('google', None)
if goog is not None:
return 'http://books.google.com/books?id=%s'%goog
return ('google', goog, 'http://books.google.com/books?id=%s'%goog)
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{

View File

@ -435,18 +435,30 @@ def identify(log, abort, # {{{
# }}}
def urls_from_identifiers(identifiers): # {{{
identifiers = dict([(k.lower(), v) for k, v in identifiers.iteritems()])
ans = []
for plugin in all_metadata_plugins():
try:
url = plugin.get_book_url(identifiers)
if url is not None:
ans.append((plugin.name, url))
id_type, id_val, url = plugin.get_book_url(identifiers)
ans.append((plugin.name, id_type, id_val, url))
except:
pass
isbn = identifiers.get('isbn', None)
if isbn:
ans.append((isbn,
'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn))
ans.append((isbn, 'isbn', isbn,
'http://www.worldcat.org/isbn/'+isbn))
doi = identifiers.get('doi', None)
if doi:
ans.append(('DOI', 'doi', doi,
'http://dx.doi.org/'+doi))
arxiv = identifiers.get('arxiv', None)
if arxiv:
ans.append(('arXiv', 'arxiv', arxiv,
'http://arxiv.org/abs/'+arxiv))
oclc = identifiers.get('oclc', None)
if oclc:
ans.append(('OCLC', 'oclc', oclc,
'http://www.worldcat.org/oclc/'+oclc))
return ans
# }}}

View File

@ -272,7 +272,7 @@ class OverDrive(Source):
creators = creators.split(', ')
# if an exact match in a preferred format occurs
if ((author and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage:
if ((author and creators and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage:
return self.format_results(reserveid, od_title, subtitle, series, publisher,
creators, thumbimage, worldcatlink, formatid)
else:
@ -298,7 +298,7 @@ class OverDrive(Source):
close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
else:
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
elif close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900]:
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))

View File

@ -222,7 +222,7 @@ class SaveWorker(Thread):
if isbytestring(fpath):
fpath = fpath.decode(filesystem_encoding)
formats[fmt.lower()] = fpath
data[i] = [opf, cpath, formats]
data[i] = [opf, cpath, formats, mi.last_modified.isoformat()]
return data
def run(self):

View File

@ -16,6 +16,7 @@ from calibre import CurrentDir
from calibre.ebooks.pdb.formatreader import FormatReader
from calibre.ptempfile import TemporaryFile
from calibre.utils.magick import Image, create_canvas
from calibre.ebooks.compression.palmdoc import decompress_doc
DATATYPE_PHTML = 0
DATATYPE_PHTML_COMPRESSED = 1

View File

@ -739,12 +739,6 @@ def build_forms(srcdir, info=None):
dat = dat.replace('from QtWebKit.QWebView import QWebView',
'from PyQt4 import QtWebKit\nfrom PyQt4.QtWebKit import QWebView')
if form.endswith('viewer%smain.ui'%os.sep):
info('\t\tPromoting WebView')
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
dat = dat.replace('self.view = QWebView(', 'self.view = DocumentView(')
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
open(compiled_form, 'wb').write(dat)
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None)

View File

@ -17,13 +17,13 @@ from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.book.base import (field_metadata, Metadata)
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
from calibre.constants import filesystem_encoding
from calibre.library.comments import comments_to_html
from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data,
gprefs)
from calibre.utils.icu import sort_key
def render_html(mi, css, vertical, widget, all_fields=False): # {{{
table = render_data(mi, all_fields=all_fields,
use_roman_numbers=config['use_roman_numerals_for_series_number'])
@ -114,17 +114,20 @@ def render_data(mi, use_roman_numbers=True, all_fields=False):
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name,
u', '.join(fmts))))
elif field == 'identifiers':
pass # TODO
urls = urls_from_identifiers(mi.identifiers)
links = [u'<a href="%s" title="%s:%s">%s</a>' % (url, id_typ, id_val, name)
for name, id_typ, id_val, url in urls]
links = u', '.join(links)
if links:
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(
_('Ids')+':', links)))
else:
val = mi.format_field(field)[-1]
if val is None:
continue
val = prepare_string_for_xml(val)
if metadata['datatype'] == 'series':
if metadata['is_custom']:
sidx = mi.get_extra(field)
else:
sidx = getattr(mi, field+'_index')
sidx = mi.get(field+'_index')
if sidx is None:
sidx = 1.0
val = _('Book %s of <span class="series_name">%s</span>')%(fmt_sidx(sidx,
@ -292,6 +295,8 @@ class BookInfo(QWebView):
def link_activated(self, link):
self._link_clicked = True
if unicode(link.scheme()) in ('http', 'https'):
return open_url(link)
link = unicode(link.toString())
self.link_clicked.emit(link)

View File

@ -22,6 +22,12 @@
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="pixmap">
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
</property>
@ -46,6 +52,10 @@
<property name="text">
<string>Library</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
</property>
</widget>
</item>
<item>
@ -53,6 +63,10 @@
<property name="text">
<string>Device</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/reader.png</normaloff>:/images/reader.png</iconset>
</property>
</widget>
</item>
<item>
@ -60,6 +74,10 @@
<property name="text">
<string>Library and Device</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
<item>

View File

@ -214,7 +214,6 @@ class SearchBar(QWidget): # {{{
x.setIcon(QIcon(I("search_add_saved.png")))
x.setObjectName("save_search_button")
l.addWidget(x)
x.setToolTip(_("Save current search under the name shown in the box"))
# }}}

View File

@ -97,18 +97,25 @@ class RatingDelegate(QStyledItemDelegate): # {{{
class DateDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent, tweak_name='gui_timestamp_display_format',
default_format='dd MMM yyyy', editor_format='dd MMM yyyy'):
QStyledItemDelegate.__init__(self, parent)
self.tweak_name = tweak_name
self.default_format = default_format
self.editor_format = editor_format
def displayText(self, val, locale):
d = val.toDate()
if d <= UNDEFINED_QDATE:
return ''
format = tweaks['gui_timestamp_display_format']
format = tweaks[self.tweak_name]
if format is None:
format = 'dd MMM yyyy'
format = self.default_format
return format_date(d.toPyDate(), format)
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('dd MMM yyyy')
qde.setDisplayFormat(self.editor_format)
qde.setMinimumDate(UNDEFINED_QDATE)
qde.setSpecialValueText(_('Undefined'))
qde.setCalendarPopup(True)

View File

@ -70,6 +70,7 @@ class BooksModel(QAbstractTableModel): # {{{
'publisher' : _("Publisher"),
'tags' : _("Tags"),
'series' : _("Series"),
'last_modified' : _('Modified'),
}
def __init__(self, parent=None, buffer=40):
@ -620,6 +621,8 @@ class BooksModel(QAbstractTableModel): # {{{
idx=self.db.field_metadata['timestamp']['rec_index']),
'pubdate' : functools.partial(datetime_type,
idx=self.db.field_metadata['pubdate']['rec_index']),
'last_modified': functools.partial(datetime_type,
idx=self.db.field_metadata['last_modified']['rec_index']),
'rating' : functools.partial(rating_type,
idx=self.db.field_metadata['rating']['rec_index']),
'publisher': functools.partial(text_type,

View File

@ -76,6 +76,8 @@ class BooksView(QTableView): # {{{
self.rating_delegate = RatingDelegate(self)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
self.last_modified_delegate = DateDelegate(self,
tweak_name='gui_last_modified_display_format')
self.tags_delegate = CompleteDelegate(self, ',', 'all_tags')
self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True)
@ -296,6 +298,7 @@ class BooksView(QTableView): # {{{
state = {}
state['hidden_columns'] = [cm[i] for i in range(h.count())
if h.isSectionHidden(i) and cm[i] != 'ondevice']
state['last_modified_injected'] = True
state['sort_history'] = \
self.cleanup_sort_history(self.model().sort_history)
state['column_positions'] = {}
@ -380,7 +383,7 @@ class BooksView(QTableView): # {{{
def get_default_state(self):
old_state = {
'hidden_columns': [],
'hidden_columns': ['last_modified'],
'sort_history':[DEFAULT_SORT],
'column_positions': {},
'column_sizes': {},
@ -388,6 +391,7 @@ class BooksView(QTableView): # {{{
'size':'center',
'timestamp':'center',
'pubdate':'center'},
'last_modified_injected': True,
}
h = self.column_header
cm = self.column_map
@ -398,7 +402,7 @@ class BooksView(QTableView): # {{{
old_state['column_sizes'][name] = \
min(350, max(self.sizeHintForColumn(i),
h.sectionSizeHint(i)))
if name == 'timestamp':
if name in ('timestamp', 'last_modified'):
old_state['column_sizes'][name] += 12
return old_state
@ -418,6 +422,13 @@ class BooksView(QTableView): # {{{
pass
if ans is not None:
db.prefs[name] = ans
else:
if not ans.get('last_modified_injected', False):
ans['last_modified_injected'] = True
hc = ans.get('hidden_columns', [])
if 'last_modified' not in hc:
hc.append('last_modified')
db.prefs[name] = ans
return ans
@ -459,7 +470,8 @@ class BooksView(QTableView): # {{{
def database_changed(self, db):
for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) in (self.rating_delegate,
self.timestamp_delegate, self.pubdate_delegate):
self.timestamp_delegate, self.pubdate_delegate,
self.last_modified_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate())
cm = self.column_map

View File

@ -18,11 +18,11 @@ from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors, check_isbn
from calibre.ebooks.metadata import (title_sort, authors_to_string,
string_to_authors, check_isbn)
from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog
from calibre.gui2 import (file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE,
choose_files, error_dialog, choose_images, question_dialog)
from calibre.utils.date import local_tz, qt_to_dt
from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS
@ -280,11 +280,16 @@ class AuthorSortEdit(EnLineEdit):
aus = self.current_val
meth = tweaks['author_sort_copy_method']
if aus:
ln, _, rest = aus.partition(',')
if rest:
if meth in ('invert', 'nocomma', 'comma'):
aus = rest.strip() + ' ' + ln.strip()
self.authors_edit.current_val = [aus]
ans = []
for one in [a.strip() for a in aus.split('&')]:
if not one:
continue
ln, _, rest = one.partition(',')
if rest:
if meth in ('invert', 'nocomma', 'comma'):
one = rest.strip() + ' ' + ln.strip()
ans.append(one)
self.authors_edit.current_val = ans
def auto_generate(self, *args):
au = unicode(self.authors_edit.text())
@ -805,6 +810,7 @@ class CommentsEdit(Editor): # {{{
else:
val = comments_to_html(val)
self.html = val
self.wyswyg_dirtied()
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
@ -936,7 +942,11 @@ class IdentifiersEdit(QLineEdit): # {{{
ans = {}
for x in parts:
c = x.split(':')
if len(c) == 2:
if len(c) > 1:
if c[0] == 'isbn':
v = check_isbn(c[1])
if v is not None:
c[1] = v
ans[c[0]] = c[1]
return ans
def fset(self, val):
@ -947,6 +957,11 @@ class IdentifiersEdit(QLineEdit): # {{{
if x == 'isbn':
x = '00isbn'
return x
for k in list(val):
if k == 'isbn':
v = check_isbn(k)
if v is not None:
val[k] = v
ids = sorted(val.iteritems(), key=keygen)
txt = ', '.join(['%s:%s'%(k, v) for k, v in ids])
self.setText(txt.strip())
@ -954,8 +969,8 @@ class IdentifiersEdit(QLineEdit): # {{{
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
self.current_val = db.get_identifiers(id_, index_is_id=True)
self.original_val = self.current_val
self.original_val = db.get_identifiers(id_, index_is_id=True)
self.current_val = self.original_val
def commit(self, db, id_):
if self.original_val != self.current_val:

View File

@ -41,8 +41,11 @@ class FieldsModel(FM): # {{{
self.reset()
def commit(self):
val = [k for k, v in self.overrides.iteritems() if v == Qt.Unchecked]
self.prefs['ignore_fields'] = val
ignored_fields = set([x for x in self.prefs['ignore_fields'] if x not in
self.overrides])
changed = set([k for k, v in self.overrides.iteritems() if v ==
Qt.Unchecked])
self.prefs['ignore_fields'] = list(ignored_fields.union(changed))
# }}}

View File

@ -253,7 +253,7 @@ class ResultsView(QTableView): # {{{
parts.append('</center>')
if book.identifiers:
urls = urls_from_identifiers(book.identifiers)
ids = ['<a href="%s">%s</a>'%(url, name) for name, url in urls]
ids = ['<a href="%s">%s</a>'%(url, name) for name, ign, ign, url in urls]
if ids:
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
if book.tags:

View File

@ -63,7 +63,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.shortcuts.linkActivated.connect(self.shortcut_activated)
text = '<p>'+_('Quick create:')
for col, name in [('isbn', _('ISBN')), ('formats', _('Formats')),
('last_modified', _('Modified Date')), ('yesno', _('Yes/No')),
('yesno', _('Yes/No')),
('tags', _('Tags')), ('series', _('Series')), ('rating',
_('Rating')), ('people', _("People's names"))]:
text += ' <a href="col:%s">%s</a>,'%(col, name)
@ -150,7 +150,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'tags': _('My Tags'),
'series': _('My Series'),
'rating': _('My Rating'),
'last_modified':_('Modified Date'),
'people': _('People')}[which])
self.is_names.setChecked(which == 'people')
if self.composite_box.isVisible():
@ -158,9 +157,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
{
'isbn': '{identifiers:select(isbn)}',
'formats': '{formats}',
'last_modified':'''{last_modified:'format_date($, "dd MMM yyyy")'}'''
}[which])
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
self.composite_sort_by.setCurrentIndex(0)
def datatype_changed(self, *args):
try:

View File

@ -235,6 +235,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
gui.library_view.refresh_book_details()
if __name__ == '__main__':
app = QApplication([])
from calibre.gui2 import Application
app = Application([])
test_widget('Interface', 'Look & Feel')

View File

@ -7,28 +7,27 @@
<x>0</x>
<y>0</y>
<width>717</width>
<height>444</height>
<height>390</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QToolBox" name="toolBox">
<widget class="QWidget" name="page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<string>Main interface</string>
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<attribute name="title">
<string>Main Interface</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
@ -75,6 +74,13 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="opt_disable_animations">
<property name="toolTip">
@ -85,6 +91,13 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_disable_tray_notification">
<property name="text">
<string>Disable &amp;notifications in system tray</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="opt_show_splash_screen">
<property name="text">
@ -97,12 +110,12 @@
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Icon size:</string>
</property>
@ -115,7 +128,7 @@
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
@ -127,20 +140,7 @@
</layout>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="0">
<item row="4" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
@ -161,135 +161,15 @@
</item>
</layout>
</item>
<item row="6" column="1">
<item row="4" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_disable_tray_notification">
<property name="text">
<string>Disable &amp;notifications in system tray</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>96</height>
</rect>
</property>
<attribute name="label">
<string>Tag Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Tags browser category &amp;partitioning method:</string>
</property>
<property name="buddy">
<cstring>opt_tags_browser_partition_method</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_tags_browser_partition_method">
<property name="toolTip">
<string>Choose how tag browser subcategories are displayed when
there are more items than the limit. Select by first
letter to see an A, B, C list. Choose partitioned to
have a list of fixed-sized groups. Set to disabled
if you never want subcategories</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Collapse when more items than:</string>
</property>
<property name="buddy">
<cstring>opt_tags_browser_collapse_at</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
<property name="toolTip">
<string>If a Tag Browser category has more than this number of items, it is divided
up into sub-categories. If the partition method is set to disable, this value is ignored.</string>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_81">
<property name="text">
<string>Categories with &amp;hierarchical items:</string>
</property>
<property name="buddy">
<cstring>opt_categories_using_hierarchy</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
<property name="toolTip">
<string>A comma-separated list of columns in which items containing
periods are displayed in the tag browser trees. For example, if
this box contains 'tags' then tags of the form 'Mystery.English'
and 'Mystery.Thriller' will be displayed with English and Thriller
both under 'Mystery'. If 'tags' is not in this box,
then the tags will be displayed each on their own line.</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<spacer name="verticalSpacer_2">
<item row="6" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@ -303,77 +183,15 @@ then the tags will be displayed each on their own line.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>429</width>
<height>63</height>
</rect>
</property>
<attribute name="label">
<string>Cover Browser</string>
<widget class="QWidget" name="tab_4">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/book.png</normaloff>:/images/book.png</iconset>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_4">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<attribute name="title">
<string>Book Details</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="1">
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<layout class="QGridLayout" name="gridLayout_12">
<item row="0" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
@ -425,6 +243,16 @@ then the tags will be displayed each on their own line.</string>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
@ -437,6 +265,148 @@ then the tags will be displayed each on their own line.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/tags.png</normaloff>:/images/tags.png</iconset>
</attribute>
<attribute name="title">
<string>Tag Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Tags browser category &amp;partitioning method:</string>
</property>
<property name="buddy">
<cstring>opt_tags_browser_partition_method</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="opt_tags_browser_partition_method">
<property name="toolTip">
<string>Choose how tag browser subcategories are displayed when
there are more items than the limit. Select by first
letter to see an A, B, C list. Choose partitioned to
have a list of fixed-sized groups. Set to disabled
if you never want subcategories</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLabel" name="label_10">
<property name="text">
<string>&amp;Collapse when more items than:</string>
</property>
<property name="buddy">
<cstring>opt_tags_browser_collapse_at</cstring>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
<property name="toolTip">
<string>If a Tag Browser category has more than this number of items, it is divided
up into sub-categories. If the partition method is set to disable, this value is ignored.</string>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="1" column="0" colspan="5">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_81">
<property name="text">
<string>Categories with &amp;hierarchical items:</string>
</property>
<property name="buddy">
<cstring>opt_categories_using_hierarchy</cstring>
</property>
</widget>
</item>
<item row="3" column="0" colspan="5">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>690</width>
<height>252</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="2" colspan="3">
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
<property name="toolTip">
<string>A comma-separated list of columns in which items containing
periods are displayed in the tag browser trees. For example, if
this box contains 'tags' then tags of the form 'Mystery.English'
and 'Mystery.Thriller' will be displayed with English and Thriller
both under 'Mystery'. If 'tags' is not in this box,
then the tags will be displayed each on their own line.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/cover_flow.png</normaloff>:/images/cover_flow.png</iconset>
</attribute>
<attribute name="title">
<string>Cover Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_11">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>690</width>
<height>283</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

@ -89,7 +89,7 @@ class Category(QWidget): # {{{
self.bar = QToolBar(self)
self.bar.setStyleSheet(
'QToolBar { border: none; background: none }')
self.bar.setIconSize(QSize(48, 48))
self.bar.setIconSize(QSize(32, 32))
self.bar.setMovable(False)
self.bar.setFloatable(False)
self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)

View File

@ -209,8 +209,11 @@ class FieldsModel(QAbstractListModel): # {{{
return ret
def commit(self):
val = [k for k, v in self.overrides.iteritems() if v == Qt.Unchecked]
msprefs['ignore_fields'] = val
ignored_fields = set([x for x in msprefs['ignore_fields'] if x not in
self.overrides])
changed = set([k for k, v in self.overrides.iteritems() if v ==
Qt.Unchecked])
msprefs['ignore_fields'] = list(ignored_fields.union(changed))
# }}}

View File

@ -7,12 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from functools import partial
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \
pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, \
QString, QIcon
QString, QIcon, QMenu
from calibre.gui2 import config
from calibre.gui2 import config, error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.search_query_parser import saved_searches
@ -330,6 +333,24 @@ class SavedSearchBox(QComboBox): # {{{
self.saved_search_selected (name)
self.changed.emit()
def delete_current_search(self):
idx = self.currentIndex()
if idx <= 0:
error_dialog(self, _('Delete current search'),
_('No search is selected'), show=True)
return
if not confirm('<p>'+_('The selected search will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'saved_search_delete', self):
return
ss = saved_searches().lookup(unicode(self.currentText()))
if ss is None:
return
saved_searches().delete(unicode(self.currentText()))
self.clear()
self.search_box.clear()
self.changed.emit()
# SIGNALed from the main UI
def copy_search_button_clicked (self):
idx = self.currentIndex();
@ -428,6 +449,22 @@ class SavedSearchBoxMixin(object): # {{{
for x in ('copy', 'save'):
b = getattr(self, x+'_search_button')
b.setStatusTip(b.toolTip())
self.save_search_button.setToolTip('<p>' +
_("Save current search under the name shown in the box. "
"Press and hold for a pop-up options menu.") + '</p>')
self.save_search_button.setMenu(QMenu())
self.save_search_button.menu().addAction(
QIcon(I('plus.png')),
_('Create saved search'),
self.saved_search.save_search_button_clicked)
self.save_search_button.menu().addAction(
QIcon(I('trash.png')),
_('Delete saved search'),
self.saved_search.delete_current_search)
self.save_search_button.menu().addAction(
QIcon(I('search.png')),
_('Manage saved searches'),
partial(self.do_saved_search_edit, None))
def saved_searches_changed(self, set_restriction=None, recount=True):
p = sorted(saved_searches().names(), key=sort_key)

View File

@ -33,24 +33,21 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QWebView" name="view"/>
</item>
<item row="0" column="1">
<item row="1" column="1">
<widget class="QScrollBar" name="vertical_scrollbar">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QScrollBar" name="horizontal_scrollbar">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QFrame" name="dictionary_box">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
@ -91,6 +88,9 @@
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="DocumentView" name="view" native="true"/>
</item>
</layout>
</widget>
</widget>
@ -108,7 +108,7 @@
</size>
</property>
<attribute name="toolBarArea">
<enum>Qt::LeftToolBarArea</enum>
<enum>LeftToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
@ -136,7 +136,7 @@
</widget>
<widget class="QToolBar" name="tool_bar2">
<attribute name="toolBarArea">
<enum>Qt::TopToolBarArea</enum>
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
@ -316,6 +316,12 @@
<extends>QWidget</extends>
<header>QtWebKit/QWebView</header>
</customwidget>
<customwidget>
<class>DocumentView</class>
<extends>QWidget</extends>
<header>calibre/gui2/viewer/documentview.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>

View File

@ -406,11 +406,9 @@ class ResultCache(SearchQueryParser): # {{{
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']
q = ''
val_func = lambda item, loc=loc: item[loc]
cast = adjust = lambda x: x
dt = self.field_metadata[location]['datatype']
if query == 'false':
if dt == 'rating' or location == 'cover':

View File

@ -368,7 +368,8 @@ class FieldMetadata(dict):
'date_format': tweaks['gui_timestamp_display_format']}
self._tb_cats['pubdate']['display'] = {
'date_format': tweaks['gui_pubdate_display_format']}
self._tb_cats['last_modified']['display'] = {'date_format': 'iso'}
self._tb_cats['last_modified']['display'] = {
'date_format': tweaks['gui_last_modified_display_format']}
self.custom_field_prefix = '#'
self.get = self._tb_cats.get

View File

@ -17,6 +17,7 @@ from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.constants import preferred_encoding
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata import title_sort
from calibre.utils.date import parse_date
from calibre import strftime, prints, sanitize_file_name_unicode
plugboard_any_device_value = 'any device'
@ -42,6 +43,8 @@ FORMAT_ARG_DESCS = dict(
publisher=_('The publisher'),
timestamp=_('The date'),
pubdate=_('The published date'),
last_modified=_('The date when the metadata for this book record'
' was last modified'),
id=_('The calibre internal id')
)
@ -191,6 +194,9 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
if hasattr(mi.pubdate, 'timetuple'):
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
if hasattr(mi, 'last_modified') and hasattr(mi.last_modified, 'timetuple'):
format_args['last_modified'] = strftime(timefmt, mi.last_modified.timetuple())
format_args['id'] = str(id)
# Now format the custom fields
custom_metadata = mi.get_all_user_metadata(make_copy=False)
@ -373,10 +379,14 @@ def save_serialized_to_disk(ids, data, plugboards, root, opts, callback):
root, opts, length = _sanitize_args(root, opts)
failures = []
for x in ids:
opf, cover, format_map = data[x]
opf, cover, format_map, last_modified = data[x]
if isinstance(opf, unicode):
opf = opf.encode('utf-8')
mi = OPF(cStringIO.StringIO(opf)).to_book_metadata()
try:
mi.last_modified = parse_date(last_modified)
except:
pass
tb = ''
try:
failed, id, title = do_save_book_to_disk(x, mi, cover, plugboards,

View File

@ -20,13 +20,14 @@ What formats does |app| support conversion to/from?
|app| supports the conversion of many input formats to many output formats.
It can convert every input format in the following list, to every output format.
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB***, PML, RB, RTF, SNB, TCR, TXT, TXTZ
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
.. note ::
*** PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files.
PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers.
PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files.
.. _best-source-formats:

View File

@ -365,6 +365,8 @@ Dates and numeric fields support the relational operators ``=`` (equals), ``>``
Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3
or higher.
You can search for the number of items in multiple-valued fields such as tags). These searches begin with the character ``#``, then use the same syntax as numeric fields. For example, to find all books with more than 4 tags, use ``tags:#>4``. To find all books with exactly 10 tags, use ``tags:#=10``.
Series indices are searchable. For the standard series, the search name is 'series_index'. For
custom series columns, use the column search name followed by _index. For example, to search the indices for a
custom series column named ``#my_series``, you would use the search name ``#my_series_index``.

View File

@ -65,17 +65,14 @@ Catalog plugins
Metadata download plugins
--------------------------
.. module:: calibre.ebooks.metadata.fetch
.. module:: calibre.ebooks.metadata.sources.base
.. autoclass:: MetadataSource
.. autoclass:: Source
:show-inheritance:
:members:
:member-order: bysource
.. autoclass:: calibre.ebooks.metadata.covers.CoverDownload
:show-inheritance:
:members:
:member-order: bysource
.. autoclass:: InternalMetadataCompareKeyGen
Conversion plugins
--------------------