Virtual Libraries

This commit is contained in:
Kovid Goyal 2013-04-13 09:26:48 +05:30
commit a8d9d760ff
17 changed files with 653 additions and 144 deletions

View File

@ -422,6 +422,8 @@ class DB(object):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False), ('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False), ('last_modified', False), ('size', False), ('languages', False),
] ]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak # Migrate the bool tristate tweak
defs['bools_are_tristate'] = \ defs['bools_are_tristate'] = \
@ -470,6 +472,24 @@ class DB(object):
except: except:
pass pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case # Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', []) user_cats = self.prefs.get('user_categories', [])
catmap = {} catmap = {}

View File

@ -49,7 +49,8 @@ class View(object):
self.cache = cache self.cache = cache
self.marked_ids = {} self.marked_ids = {}
self.search_restriction_book_count = 0 self.search_restriction_book_count = 0
self.search_restriction = '' self.search_restriction = self.base_restriction = ''
self.search_restriction_name = self.base_restriction_name = ''
self._field_getters = {} self._field_getters = {}
for col, idx in cache.backend.FIELD_MAP.iteritems(): for col, idx in cache.backend.FIELD_MAP.iteritems():
if isinstance(col, int): if isinstance(col, int):
@ -168,8 +169,19 @@ class View(object):
return ans return ans
self._map_filtered = tuple(ans) self._map_filtered = tuple(ans)
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction, def search_getting_ids(self, query, search_restriction,
set_restriction_count=False): set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = '' q = ''
if not query or not query.strip(): if not query or not query.strip():
q = search_restriction q = search_restriction
@ -188,11 +200,32 @@ class View(object):
self.search_restriction_book_count = len(rv) self.search_restriction_book_count = len(rv)
return rv return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self): def search_restriction_applied(self):
return bool(self.search_restriction) return bool(self.search_restriction) or bool(self.base_restriction)
def get_search_restriction_book_count(self): def get_search_restriction_book_count(self):
return self.search_restriction_book_count return self.search_restriction_book_count

View File

@ -16,7 +16,6 @@ from calibre.constants import __appname__
from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2.bars import BarsManager from calibre.gui2.bars import BarsManager
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.config_base import tweaks from calibre.utils.config_base import tweaks
from calibre import human_readable from calibre import human_readable
@ -173,11 +172,13 @@ class SearchBar(QWidget): # {{{
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0,5,0,0) self._layout.setContentsMargins(0,5,0,0)
x = ComboBoxWithHelp(self) x = QToolButton(self)
x.setMaximumSize(QSize(150, 16777215)) x.setText(_('Virtual Library'))
x.setObjectName("search_restriction") x.setIcon(QIcon(I('lt.png')))
x.setObjectName("virtual_library")
x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
l.addWidget(x) l.addWidget(x)
parent.search_restriction = x parent.virtual_library = x
x = QLabel(self) x = QLabel(self)
x.setObjectName("search_count") x.setObjectName("search_count")

View File

@ -14,7 +14,6 @@ from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.gui2 import config, info_dialog, dynamic, gprefs from calibre.gui2 import config, info_dialog, dynamic, gprefs
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows from calibre.constants import iswindows
@ -48,9 +47,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
choices = [(x.upper(), x) for x in output_formats] choices = [(x.upper(), x) for x in output_formats]
r('output_format', prefs, choices=choices, setting=OutputFormatSetting) r('output_format', prefs, choices=choices, setting=OutputFormatSetting)
restrictions = sorted(saved_searches().names(), key=sort_key) restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
choices = [('', '')] + [(x, x) for x in restrictions] choices = [('', '')] + [(x, x) for x in restrictions]
r('gui_restriction', db.prefs, choices=choices) # check that the virtual library still exists
vls = db.prefs['virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['virtual_lib_on_startup'] = ''
r('virtual_lib_on_startup', db.prefs, choices=choices)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs) self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input) self.input_up_button.clicked.connect(self.up_input)

View File

@ -147,15 +147,15 @@ If not checked, the values can be Yes or No.</string>
<item> <item>
<widget class="QLabel" name="label_170"> <widget class="QLabel" name="label_170">
<property name="text"> <property name="text">
<string>Restriction to apply when the current library is opened:</string> <string>Virtual library to apply when the current library is opened:</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>opt_gui_restriction</cstring> <cstring>opt_virtual_lib_on_startup</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="opt_gui_restriction"> <widget class="QComboBox" name="opt_virtual_lib_on_startup">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>250</width> <width>250</width>
@ -163,7 +163,7 @@ If not checked, the values can be Yes or No.</string>
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>Apply this restriction on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string> <string>Use this virtual library on calibre startup if the current library is being used. Also applied when switching to this library. Note that this setting is per library. </string>
</property> </property>
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -12,7 +12,6 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.server_ui import Ui_Form from calibre.gui2.preferences.server_ui import Ui_Form
from calibre.utils.search_query_parser import saved_searches
from calibre.library.server import server_config from calibre.library.server import server_config
from calibre.utils.config import ConfigProxy from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
@ -44,13 +43,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else self.opt_password.Password)) else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password) self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(saved_searches().names(), key=sort_key) restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
# verify that the current restriction still exists. If not, clear it.
csr = db.prefs.get('cs_restriction', None)
if csr and csr not in restrictions:
db.prefs.set('cs_restriction', '')
choices = [('', '')] + [(x, x) for x in restrictions] choices = [('', '')] + [(x, x) for x in restrictions]
r('cs_restriction', db.prefs, choices=choices) # check that the virtual library still exists
vls = db.prefs['cs_virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['cs_virtual_lib_on_startup'] = ''
r('cs_virtual_lib_on_startup', db.prefs, choices=choices)
self.start_button.setEnabled(not getattr(self.server, 'is_running', False)) self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
self.test_button.setEnabled(not self.start_button.isEnabled()) self.test_button.setEnabled(not self.start_button.isEnabled())

View File

@ -139,14 +139,14 @@
<item row="7" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_164"> <widget class="QLabel" name="label_164">
<property name="text"> <property name="text">
<string>Restriction (saved search) to apply:</string> <string>Virtual library to apply:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1" colspan="2"> <item row="7" column="1" colspan="2">
<widget class="QComboBox" name="opt_cs_restriction"> <widget class="QComboBox" name="opt_cs_virtual_lib_on_startup">
<property name="toolTip"> <property name="toolTip">
<string>This restriction (based on a saved search) will restrict the books the content server makes available to those matching the search. This setting is per library (i.e. you can have a different restriction per library).</string> <string>Setting a virtual library will restrict the books the content server makes available to those in the library. This setting is per library (i.e. you can have a different value per library).</string>
</property> </property>
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>

View File

@ -19,7 +19,6 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
class SearchLineEdit(QLineEdit): # {{{ class SearchLineEdit(QLineEdit): # {{{
key_pressed = pyqtSignal(object) key_pressed = pyqtSignal(object)
@ -332,6 +331,10 @@ class SavedSearchBox(QComboBox): # {{{
name = unicode(self.currentText()) name = unicode(self.currentText())
if not name.strip(): if not name.strip():
name = unicode(self.search_box.text()).replace('"', '') name = unicode(self.search_box.text()).replace('"', '')
if not (name and self.search_box.text()):
error_dialog(self, _('Create saved search'),
_('There is no search to save'), show=True)
return
saved_searches().delete(name) saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text())) saved_searches().add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has # now go through an initialization cycle to ensure that the combobox has
@ -339,7 +342,7 @@ class SavedSearchBox(QComboBox): # {{{
# references the new search instead of the text in the search. # references the new search instead of the text in the search.
self.clear() self.clear()
self.setCurrentIndex(self.findText(name)) self.setCurrentIndex(self.findText(name))
self.saved_search_selected (name) self.saved_search_selected(name)
self.changed.emit() self.changed.emit()
def delete_current_search(self): def delete_current_search(self):
@ -361,8 +364,8 @@ class SavedSearchBox(QComboBox): # {{{
self.changed.emit() self.changed.emit()
# SIGNALed from the main UI # SIGNALed from the main UI
def copy_search_button_clicked (self): def copy_search_button_clicked(self):
idx = self.currentIndex(); idx = self.currentIndex()
if idx < 0: if idx < 0:
return return
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText()))) self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
@ -452,7 +455,7 @@ class SavedSearchBoxMixin(object): # {{{
self.saved_search.save_search_button_clicked) self.saved_search.save_search_button_clicked)
self.copy_search_button.clicked.connect( self.copy_search_button.clicked.connect(
self.saved_search.copy_search_button_clicked) self.saved_search.copy_search_button_clicked)
self.saved_searches_changed() # self.saved_searches_changed()
self.saved_search.initialize(self.search, colorize=True, self.saved_search.initialize(self.search, colorize=True,
help_text=_('Saved Searches')) help_text=_('Saved Searches'))
self.saved_search.setToolTip( self.saved_search.setToolTip(
@ -479,17 +482,9 @@ class SavedSearchBoxMixin(object): # {{{
partial(self.do_saved_search_edit, None)) partial(self.do_saved_search_edit, None))
def saved_searches_changed(self, set_restriction=None, recount=True): def saved_searches_changed(self, set_restriction=None, recount=True):
p = sorted(saved_searches().names(), key=sort_key) self.build_search_restriction_list()
if set_restriction is None:
set_restriction = unicode(self.search_restriction.currentText())
# rebuild the restrictions combobox using current saved searches
self.search_restriction.clear()
self.search_restriction.addItem('')
self.search_restriction.addItem(_('*Current search'))
if recount: if recount:
self.tags_view.recount() self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if set_restriction: # redo the search restriction if there was one if set_restriction: # redo the search restriction if there was one
self.apply_named_search_restriction(set_restriction) self.apply_named_search_restriction(set_restriction)

View File

@ -4,23 +4,422 @@ Created on 10 Jun 2010
@author: charles @author: charles
''' '''
from PyQt4.Qt import Qt from functools import partial
from PyQt4.Qt import (
Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit,
QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList, QCheckBox)
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre.utils.icu import sort_key
from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import saved_searches
class SelectNames(QDialog): # {{{
def __init__(self, names, txt, parent=None):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.la = la = QLabel(_('Create a Virtual Library based on %s') % txt)
l.addWidget(la)
self._names = QListWidget(self)
self._names.addItems(QStringList(sorted(names, key=sort_key)))
self._names.setSelectionMode(self._names.ExtendedSelection)
l.addWidget(self._names)
self._and = QCheckBox(_('Match all selected %s names')%txt)
l.addWidget(self._and)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
l.addWidget(self.bb)
self.resize(self.sizeHint())
@property
def names(self):
for item in self._names.selectedItems():
yield unicode(item.data(Qt.DisplayRole).toString())
@property
def match_type(self):
return ' and ' if self._and.isChecked() else ' or '
# }}}
MAX_VIRTUAL_LIBRARY_NAME_LENGTH = 40
def _build_full_search_string(gui):
search_templates = (
'',
'{cl}',
'{cr}',
'(({cl}) and ({cr}))',
'{sb}',
'(({cl}) and ({sb}))',
'(({cr}) and ({sb}))',
'(({cl}) and ({cr}) and ({sb}))'
)
sb = gui.search.current_text
db = gui.current_db
cr = db.data.get_search_restriction()
cl = db.data.get_base_restriction()
dex = 0
if sb:
dex += 4
if cr:
dex += 2
if cl:
dex += 1
template = search_templates[dex]
return template.format(cl=cl, cr=cr, sb=sb).strip()
class CreateVirtualLibrary(QDialog): # {{{
def __init__(self, gui, existing_names, editing=None):
QDialog.__init__(self, gui)
self.gui = gui
self.existing_names = existing_names
if editing:
self.setWindowTitle(_('Edit virtual library'))
else:
self.setWindowTitle(_('Create virtual library'))
self.setWindowIcon(QIcon(I('lt.png')))
gl = QGridLayout()
self.setLayout(gl)
self.la1 = la1 = QLabel(_('Virtual library &name:'))
gl.addWidget(la1, 0, 0)
self.vl_name = QLineEdit()
self.vl_name.setMaxLength(MAX_VIRTUAL_LIBRARY_NAME_LENGTH)
la1.setBuddy(self.vl_name)
gl.addWidget(self.vl_name, 0, 1)
self.editing = editing
if editing:
self.vl_name.setText(editing)
self.la2 = la2 = QLabel(_('&Search expression:'))
gl.addWidget(la2, 1, 0)
self.vl_text = QLineEdit()
la2.setBuddy(self.vl_text)
gl.addWidget(self.vl_text, 1, 1)
self.vl_text.setText(_build_full_search_string(self.gui))
self.sl = sl = QLabel('<p>'+_('Create a virtual library based on: ')+
('<a href="author.{0}">{0}</a>, '
'<a href="tag.{1}">{1}</a>, '
'<a href="publisher.{2}">{2}</a>, '
'<a href="series.{3}">{3}</a>.').format(_('Authors'), _('Tags'), _('Publishers'), _('Series')))
sl.setWordWrap(True)
sl.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
sl.linkActivated.connect(self.link_activated)
gl.addWidget(sl, 2, 0, 1, 2)
self.hl = hl = QLabel(_('''
<h2>Virtual Libraries</h2>
<p>Using <i>virtual libraries</i> you can restrict calibre to only show
you books that match a search. When a virtual library is in effect, calibre
behaves as though the library contains only the matched books. The Tag Browser
display only the tags/authors/series/etc. that belong to the matched books and any searches
you do will only search within the books in the virtual library. This
is a good way to partition your large library into smaller and easier to work with subsets.</p>
<p>For example you can use a Virtual Library to only show you books with the Tag <i>"Unread"</i>
or only books by <i>"My Favorite Author"</i> or only books in a particular series.</p>
'''))
hl.setWordWrap(True)
hl.setFrameStyle(hl.StyledPanel)
gl.addWidget(hl, 0, 3, 4, 1)
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
gl.addWidget(bb, 4, 0, 1, 0)
if editing:
db = self.gui.current_db
virt_libs = db.prefs.get('virtual_libraries', {})
self.vl_text.setText(virt_libs.get(editing, ''))
self.resize(self.sizeHint()+QSize(150, 25))
def link_activated(self, url):
db = self.gui.current_db
f, txt = unicode(url).partition('.')[0::2]
names = getattr(db, 'all_%s_names'%f)()
d = SelectNames(names, txt, parent=self)
if d.exec_() == d.Accepted:
prefix = f+'s' if f in {'tag', 'author'} else f
search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names]
if search:
self.vl_name.setText(d.names.next())
self.vl_text.setText(d.match_type.join(search))
self.vl_text.setCursorPosition(0)
self.vl_name.setCursorPosition(0)
def accept(self):
n = unicode(self.vl_name.text()).strip()
if not n:
error_dialog(self.gui, _('No name'),
_('You must provide a name for the new virtual library'),
show=True)
return
if n.startswith('*'):
error_dialog(self.gui, _('Invalid name'),
_('A virtual library name cannot begin with "*"'),
show=True)
return
if n in self.existing_names and n != self.editing:
if question_dialog(self.gui, _('Name already in use'),
_('That name is already in use. Do you want to replace it '
'with the new search?'),
default_yes=False) == self.Rejected:
return
v = unicode(self.vl_text.text()).strip()
if not v:
error_dialog(self.gui, _('No search string'),
_('You must provide a search to define the new virtual library'),
show=True)
return
try:
db = self.gui.library_view.model().db
recs = db.data.search_getting_ids('', v, use_virtual_library=False)
except ParseException as e:
error_dialog(self.gui, _('Invalid search'),
_('The search in the search box is not valid'),
det_msg=e.msg, show=True)
return
if not recs and not question_dialog(
self.gui, _('Search found no books'),
_('The search found no books, so the virtual library '
'will be empty. Do you really want to use that search?'),
default_yes=False):
return
self.library_name = n
self.library_search = v
QDialog.accept(self)
# }}}
class SearchRestrictionMixin(object): class SearchRestrictionMixin(object):
no_restriction = _('<None>')
def __init__(self): def __init__(self):
self.search_restriction.initialize(help_text=_('Restrict to')) self.checked = QIcon(I('ok.png'))
self.search_restriction.activated[int].connect(self.apply_search_restriction) self.empty = QIcon()
self.library_view.model().count_changed_signal.connect(self.set_number_of_books_shown) self.search_based_vl_name = None
self.search_restriction.setSizeAdjustPolicy( self.search_based_vl = None
self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10) self.virtual_library_menu = QMenu()
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
self.virtual_library.clicked.connect(self.virtual_library_clicked)
self.virtual_library_tooltip = \
_('Books display will show only those books matching the search')
self.virtual_library.setToolTip(self.virtual_library_tooltip)
self.search_restriction = ComboBoxWithHelp(self)
self.search_restriction.setVisible(False)
self.search_count.setText(_("(all books)")) self.search_count.setText(_("(all books)"))
self.search_restriction_tooltip = \ self.ar_menu = QMenu(_('Additional restriction'))
_('Books display will be restricted to those matching a '
'selected saved search') def add_virtual_library(self, db, name, search):
self.search_restriction.setToolTip(self.search_restriction_tooltip) virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs[name] = search
db.prefs.set('virtual_libraries', virt_libs)
def do_create_edit(self, editing=None):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
cd = CreateVirtualLibrary(self, virt_libs.keys(), editing=editing)
if cd.exec_() == cd.Accepted:
if editing:
self._remove_vl(editing, reapply=False)
self.add_virtual_library(db, cd.library_name, cd.library_search)
self.apply_virtual_library(cd.library_name)
def virtual_library_clicked(self):
m = self.virtual_library_menu
m.clear()
a = m.addAction(_('Create Virtual Library'))
a.triggered.connect(partial(self.do_create_edit, editing=None))
self.edit_menu = a = QMenu()
a.setTitle(_('Edit Virtual Library'))
a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=False))
m.addMenu(a)
self.rm_menu = a = QMenu()
a.setTitle(_('Remove Virtual Library'))
a.aboutToShow.connect(partial(self.build_virtual_library_list, remove=True))
m.addMenu(a)
m.addSeparator()
db = self.library_view.model().db
a = self.ar_menu
a.clear()
a.setIcon(self.checked if db.data.get_search_restriction_name() else self.empty)
a.aboutToShow.connect(self.build_search_restriction_list)
m.addMenu(a)
m.addSeparator()
current_lib = db.data.get_base_restriction_name()
if current_lib == '':
a = m.addAction(self.checked, self.no_restriction)
else:
a = m.addAction(self.empty, self.no_restriction)
a.triggered.connect(partial(self.apply_virtual_library, library=''))
a = m.addAction(self.empty, _('*current search'))
a.triggered.connect(partial(self.apply_virtual_library, library='*'))
if self.search_based_vl_name:
a = m.addAction(
self.checked if db.data.get_base_restriction_name().startswith('*')
else self.empty,
self.search_based_vl_name)
a.triggered.connect(partial(self.apply_virtual_library,
library=self.search_based_vl_name))
virt_libs = db.prefs.get('virtual_libraries', {})
for vl in sorted(virt_libs.keys(), key=sort_key):
a = m.addAction(self.checked if vl == current_lib else self.empty, vl)
a.triggered.connect(partial(self.apply_virtual_library, library=vl))
p = QPoint(0, self.virtual_library.height())
self.virtual_library_menu.popup(self.virtual_library.mapToGlobal(p))
def apply_virtual_library(self, library=None):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
if not library:
db.data.set_base_restriction('')
db.data.set_base_restriction_name('')
elif library == '*':
if not self.search.current_text:
error_dialog(self, _('No search'),
_('There is no current search to use'), show=True)
return
txt = _build_full_search_string(self)
try:
db.data.search_getting_ids('', txt, use_virtual_library=False)
except ParseException as e:
error_dialog(self, _('Invalid search'),
_('The search in the search box is not valid'),
det_msg=e.msg, show=True)
return
self.search_based_vl = txt
db.data.set_base_restriction(txt)
self.search_based_vl_name = self._trim_restriction_name('*' + txt)
db.data.set_base_restriction_name(self.search_based_vl_name)
elif library == self.search_based_vl_name:
db.data.set_base_restriction(self.search_based_vl)
db.data.set_base_restriction_name(self.search_based_vl_name)
elif library in virt_libs:
db.data.set_base_restriction(virt_libs[library])
db.data.set_base_restriction_name(library)
self._apply_search_restriction(db.data.get_search_restriction(),
db.data.get_search_restriction_name())
def build_virtual_library_list(self, remove=False):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
if remove:
m = self.rm_menu
else:
m = self.edit_menu
m.clear()
def add_action(name, search):
a = m.addAction(name)
if remove:
a.triggered.connect(partial(self.remove_vl_triggered, name=name))
else:
a.triggered.connect(partial(self.do_create_edit, editing=name))
for n in sorted(virt_libs.keys(), key=sort_key):
add_action(n, virt_libs[n])
def remove_vl_triggered(self, name=None):
if not question_dialog(self, _('Are you sure?'),
_('Are you sure you want to remove '
'the virtual library {0}').format(name),
default_yes=False):
return
self._remove_vl(name, reapply=True)
def _remove_vl(self, name, reapply=True):
db = self.library_view.model().db
virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs.pop(name, None)
db.prefs.set('virtual_libraries', virt_libs)
if reapply and db.data.get_base_restriction_name() == name:
self.apply_virtual_library('')
def _trim_restriction_name(self, name):
return name[0:MAX_VIRTUAL_LIBRARY_NAME_LENGTH].strip()
def build_search_restriction_list(self):
m = self.ar_menu
m.clear()
current_restriction_text = None
if self.search_restriction.count() > 1:
txt = unicode(self.search_restriction.itemText(2))
if txt.startswith('*'):
current_restriction_text = txt
self.search_restriction.clear()
current_restriction = self.library_view.model().db.data.get_search_restriction_name()
m.setIcon(self.checked if current_restriction else self.empty)
def add_action(txt, index):
self.search_restriction.addItem(txt)
txt = self._trim_restriction_name(txt)
if txt == current_restriction:
a = m.addAction(self.checked, txt if txt else self.no_restriction)
else:
a = m.addAction(self.empty, txt if txt else self.no_restriction)
a.triggered.connect(partial(self.search_restriction_triggered,
action=a, index=index))
add_action('', 0)
add_action(_('*current search'), 1)
dex = 2
if current_restriction_text:
add_action(current_restriction_text, 2)
dex += 1
for n in sorted(saved_searches().names(), key=sort_key):
add_action(n, dex)
dex += 1
def search_restriction_triggered(self, action=None, index=None):
self.search_restriction.setCurrentIndex(index)
self.apply_search_restriction(index)
def apply_named_search_restriction(self, name): def apply_named_search_restriction(self, name):
if not name: if not name:
@ -29,7 +428,6 @@ class SearchRestrictionMixin(object):
r = self.search_restriction.findText(name) r = self.search_restriction.findText(name)
if r < 0: if r < 0:
r = 0 r = 0
if r != self.search_restriction.currentIndex():
self.search_restriction.setCurrentIndex(r) self.search_restriction.setCurrentIndex(r)
self.apply_search_restriction(r) self.apply_search_restriction(r)
@ -37,7 +435,7 @@ class SearchRestrictionMixin(object):
search = unicode(search) search = unicode(search)
if not search: if not search:
self.search_restriction.setCurrentIndex(0) self.search_restriction.setCurrentIndex(0)
self._apply_search_restriction('') self._apply_search_restriction('', '')
else: else:
s = '*' + search s = '*' + search
if self.search_restriction.count() > 1: if self.search_restriction.count() > 1:
@ -49,10 +447,7 @@ class SearchRestrictionMixin(object):
else: else:
self.search_restriction.insertItem(2, s) self.search_restriction.insertItem(2, s)
self.search_restriction.setCurrentIndex(2) self.search_restriction.setCurrentIndex(2)
self.search_restriction.setToolTip('<p>' + self._apply_search_restriction(search, self._trim_restriction_name(s))
self.search_restriction_tooltip +
_(' or the search ') + "'" + search + "'</p>")
self._apply_search_restriction(search)
def apply_search_restriction(self, i): def apply_search_restriction(self, i):
if i == 1: if i == 1:
@ -66,18 +461,20 @@ class SearchRestrictionMixin(object):
restriction = 'search:"%s"'%(r) restriction = 'search:"%s"'%(r)
else: else:
restriction = '' restriction = ''
self._apply_search_restriction(restriction) self._apply_search_restriction(restriction, r)
def _apply_search_restriction(self, restriction): def _apply_search_restriction(self, restriction, name):
self.saved_search.clear() self.saved_search.clear()
# The order below is important. Set the restriction, force a '' search # The order below is important. Set the restriction, force a '' search
# to apply it, reset the tag browser to take it into account, then set # to apply it, reset the tag browser to take it into account, then set
# the book count. # the book count.
self.library_view.model().db.data.set_search_restriction(restriction) self.library_view.model().db.data.set_search_restriction(restriction)
self.library_view.model().db.data.set_search_restriction_name(name)
self.search.clear(emit_search=True) self.search.clear(emit_search=True)
self.tags_view.set_search_restriction(restriction) self.tags_view.recount()
self.set_number_of_books_shown() self.set_number_of_books_shown()
self.current_view().setFocus(Qt.OtherFocusReason) self.current_view().setFocus(Qt.OtherFocusReason)
self.set_window_title()
def set_number_of_books_shown(self): def set_number_of_books_shown(self):
db = self.library_view.model().db db = self.library_view.model().db
@ -86,8 +483,8 @@ class SearchRestrictionMixin(object):
rows = self.current_view().row_count() rows = self.current_view().row_count()
rbc = max(rows, db.data.get_search_restriction_book_count()) rbc = max(rows, db.data.get_search_restriction_book_count())
t = _("({0} of {1})").format(rows, rbc) t = _("({0} of {1})").format(rows, rbc)
self.search_count.setStyleSheet \ self.search_count.setStyleSheet(
('QLabel { border-radius: 8px; background-color: yellow; }') 'QLabel { border-radius: 8px; background-color: yellow; }')
else: # No restriction or not library view else: # No restriction or not library view
if not self.search.in_a_search(): if not self.search.in_a_search():
t = _("(all books)") t = _("(all books)")
@ -96,3 +493,14 @@ class SearchRestrictionMixin(object):
self.search_count.setStyleSheet( self.search_count.setStyleSheet(
'QLabel { background-color: transparent; }') 'QLabel { background-color: transparent; }')
self.search_count.setText(t) self.search_count.setText(t)
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.gui2.preferences import init_gui
app = Application([])
app
gui = init_gui()
d = CreateVirtualLibrary(gui, [])
d.exec_()

View File

@ -264,13 +264,8 @@ class TagsModel(QAbstractItemModel): # {{{
if rebuild: if rebuild:
self.rebuild_node_tree(state_map) self.rebuild_node_tree(state_map)
def set_search_restriction(self, s):
self.search_restriction = s
self.rebuild_node_tree()
def set_database(self, db): def set_database(self, db):
self.beginResetModel() self.beginResetModel()
self.search_restriction = None
hidden_cats = db.prefs.get('tag_browser_hidden_categories', None) hidden_cats = db.prefs.get('tag_browser_hidden_categories', None)
# migrate from config to db prefs # migrate from config to db prefs
if hidden_cats is None: if hidden_cats is None:
@ -848,7 +843,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories = {} self.categories = {}
# Get the categories # Get the categories
if self.search_restriction: if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
try: try:
data = self.db.get_categories(sort=sort, data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map, icon_map=self.category_icon_map,

View File

@ -232,10 +232,6 @@ class TagsView(QTreeView): # {{{
except: except:
pass pass
def set_search_restriction(self, s):
s = s if s else None
self._model.set_search_restriction(s)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
dex = self.indexAt(event.pos()) dex = self.indexAt(event.pos())
if self.in_drag_drop or not dex.isValid(): if self.in_drag_drop or not dex.isValid():

View File

@ -15,7 +15,7 @@ from threading import Thread
from collections import OrderedDict from collections import OrderedDict
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction, from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
QMenu, QIcon, pyqtSignal, QUrl, QMenu, QIcon, pyqtSignal, QUrl, QFont,
QDialog, QSystemTrayIcon, QApplication) QDialog, QSystemTrayIcon, QApplication)
from calibre import prints, force_unicode from calibre import prints, force_unicode
@ -187,7 +187,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
else: else:
stmap[st.name] = st stmap[st.name] = st
def initialize(self, library_path, db, listener, actions, show_gui=True): def initialize(self, library_path, db, listener, actions, show_gui=True):
opts = self.opts opts = self.opts
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
@ -279,6 +278,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
UpdateMixin.__init__(self, opts) UpdateMixin.__init__(self, opts)
####################### Search boxes ######################## ####################### Search boxes ########################
SearchRestrictionMixin.__init__(self)
SavedSearchBoxMixin.__init__(self) SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self)
@ -313,9 +313,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
TagBrowserMixin.__init__(self, db) TagBrowserMixin.__init__(self, db)
######################### Search Restriction ########################## ######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self) if db.prefs['virtual_lib_on_startup']:
if db.prefs['gui_restriction']: self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################ ########################### Cover Flow ################################
@ -339,7 +338,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
self.start_content_server() self.start_content_server()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings() self.read_settings()
@ -494,7 +492,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
path = os.path.abspath(argv[1]) path = os.path.abspath(argv[1])
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
self.iactions['Add Books'].add_filesystem_book(path) self.iactions['Add Books'].add_filesystem_book(path)
self.setWindowState(self.windowState() & \ self.setWindowState(self.windowState() &
~Qt.WindowMinimized|Qt.WindowActive) ~Qt.WindowMinimized|Qt.WindowActive)
self.show_windows() self.show_windows()
self.raise_() self.raise_()
@ -526,7 +524,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def library_moved(self, newloc, copy_structure=False, call_close=True, def library_moved(self, newloc, copy_structure=False, call_close=True,
allow_rebuild=False): allow_rebuild=False):
if newloc is None: return if newloc is None:
return
default_prefs = None default_prefs = None
try: try:
olddb = self.library_view.model().db olddb = self.library_view.model().db
@ -537,7 +536,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
try: try:
db = LibraryDatabase2(newloc, default_prefs=default_prefs) db = LibraryDatabase2(newloc, default_prefs=default_prefs)
except (DatabaseException, sqlite.Error): except (DatabaseException, sqlite.Error):
if not allow_rebuild: raise if not allow_rebuild:
raise
import traceback import traceback
repair = question_dialog(self, _('Corrupted database'), repair = question_dialog(self, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do ' _('The library database at %s appears to be corrupted. Do '
@ -596,9 +596,19 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# interface later # interface later
gc.collect() gc.collect()
def set_window_title(self): def set_window_title(self):
self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name()) db = self.current_db
restrictions = [x for x in (db.data.get_base_restriction_name(),
db.data.get_search_restriction_name()) if x]
restrictions = ' :: '.join(restrictions)
font = QFont()
if restrictions:
restrictions = ' :: ' + restrictions
font.setBold(True)
self.virtual_library.setFont(font)
title = u'{0} - || {1}{2} ||'.format(
__appname__, self.iactions['Choose Library'].library_name(), restrictions)
self.setWindowTitle(title)
def location_selected(self, location): def location_selected(self, location):
''' '''
@ -613,17 +623,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for action in self.iactions.values(): for action in self.iactions.values():
action.location_selected(location) action.location_selected(location)
if location == 'library': if location == 'library':
self.search_restriction.setEnabled(True) self.virtual_library_menu.setEnabled(True)
self.highlight_only_button.setEnabled(True) self.highlight_only_button.setEnabled(True)
else: else:
self.search_restriction.setEnabled(False) self.virtual_library_menu.setEnabled(False)
self.highlight_only_button.setEnabled(False) self.highlight_only_button.setEnabled(False)
# Reset the view in case something changed while it was invisible # Reset the view in case something changed while it was invisible
self.current_view().reset() self.current_view().reset()
self.set_number_of_books_shown() self.set_number_of_books_shown()
def job_exception(self, job, dialog_title=_('Conversion Error')): def job_exception(self, job, dialog_title=_('Conversion Error')):
if not hasattr(self, '_modeless_dialogs'): if not hasattr(self, '_modeless_dialogs'):
self._modeless_dialogs = [] self._modeless_dialogs = []
@ -748,7 +756,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
return False return False
return True return True
def shutdown(self, write_settings=True): def shutdown(self, write_settings=True):
try: try:
db = self.library_view.model().db db = self.library_view.model().db
@ -808,13 +815,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
pass pass
QApplication.instance().quit() QApplication.instance().quit()
def closeEvent(self, e): def closeEvent(self, e):
self.write_settings() self.write_settings()
if self.system_tray_icon.isVisible(): if self.system_tray_icon.isVisible():
if not dynamic['systray_msg'] and not isosx: if not dynamic['systray_msg'] and not isosx:
info_dialog(self, 'calibre', 'calibre '+ \ info_dialog(self, 'calibre', 'calibre '+
_('will keep running in the system tray. To close it, ' _('will keep running in the system tray. To close it, '
'choose <b>Quit</b> in the context menu of the ' 'choose <b>Quit</b> in the context menu of the '
'system tray.'), show_copy_button=False).exec_() 'system tray.'), show_copy_button=False).exec_()

View File

@ -209,7 +209,8 @@ class ResultCache(SearchQueryParser): # {{{
self._data = [] self._data = []
self._map = self._map_filtered = [] self._map = self._map_filtered = []
self.first_sort = True self.first_sort = True
self.search_restriction = '' self.search_restriction = self.base_restriction = ''
self.base_restriction_name = self.search_restriction_name = ''
self.search_restriction_book_count = 0 self.search_restriction_book_count = 0
self.marked_ids_dict = {} self.marked_ids_dict = {}
self.field_metadata = field_metadata self.field_metadata = field_metadata
@ -825,8 +826,19 @@ class ResultCache(SearchQueryParser): # {{{
return ans return ans
self._map_filtered = ans self._map_filtered = ans
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return u'(%s) and (%s)' % (self.base_restriction, restriction)
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction, def search_getting_ids(self, query, search_restriction,
set_restriction_count=False): set_restriction_count=False, use_virtual_library=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = '' q = ''
if not query or not query.strip(): if not query or not query.strip():
q = search_restriction q = search_restriction
@ -847,11 +859,32 @@ class ResultCache(SearchQueryParser): # {{{
self.search_restriction_book_count = len(rv) self.search_restriction_book_count = len(rv)
return rv return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, s): def set_search_restriction(self, s):
self.search_restriction = s self.search_restriction = s
def get_base_restriction(self):
return self.base_restriction
def set_base_restriction(self, s):
self.base_restriction = s
def get_base_restriction_name(self):
return self.base_restriction_name
def set_base_restriction_name(self, s):
self.base_restriction_name = s
def get_search_restriction_name(self):
return self.search_restriction_name
def set_search_restriction_name(self, s):
self.search_restriction_name = s
def search_restriction_applied(self): def search_restriction_applied(self):
return bool(self.search_restriction) return bool(self.search_restriction) or bool((self.base_restriction))
def get_search_restriction_book_count(self): def get_search_restriction_book_count(self):
return self.search_restriction_book_count return self.search_restriction_book_count
@ -1002,7 +1035,7 @@ class ResultCache(SearchQueryParser): # {{{
if field is not None: if field is not None:
self.sort(field, ascending) self.sort(field, ascending)
self._map_filtered = list(self._map) self._map_filtered = list(self._map)
if self.search_restriction: if self.search_restriction or self.base_restriction:
self.search('', return_matches=False) self.search('', return_matches=False)
# Sorting functions {{{ # Sorting functions {{{

View File

@ -229,6 +229,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
('uuid', False), ('comments', True), ('id', False), ('pubdate', False), ('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False), ('languages', False), ('last_modified', False), ('size', False), ('languages', False),
] ]
defs['virtual_libraries'] = {}
defs['virtual_lib_on_startup'] = defs['cs_virtual_lib_on_startup'] = ''
# Migrate the bool tristate tweak # Migrate the bool tristate tweak
defs['bools_are_tristate'] = \ defs['bools_are_tristate'] = \
@ -279,6 +281,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
except: except:
pass pass
# migrate the gui_restriction preference to a virtual library
gr_pref = self.prefs.get('gui_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['gui_restriction'] = ''
self.prefs['virtual_lib_on_startup'] = gr_pref
# migrate the cs_restriction preference to a virtual library
gr_pref = self.prefs.get('cs_restriction', None)
if gr_pref:
virt_libs = self.prefs.get('virtual_libraries', {})
virt_libs[gr_pref] = 'search:"' + gr_pref + '"'
self.prefs['virtual_libraries'] = virt_libs
self.prefs['cs_restriction'] = ''
self.prefs['cs_virtual_lib_on_startup'] = gr_pref
# Rename any user categories with names that differ only in case # Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', []) user_cats = self.prefs.get('user_categories', [])
catmap = {} catmap = {}

View File

@ -205,26 +205,32 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
virt_libs = db.prefs.get('virtual_libraries', {})
sr = getattr(self.opts, 'restriction', None) sr = getattr(self.opts, 'restriction', None)
sr = db.prefs.get('cs_restriction', '') if sr is None else sr if sr:
self.set_search_restriction(sr) if sr in virt_libs:
sr = virt_libs[sr]
elif sr not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
sr, ' does not exist')
sr = ''
else:
sr = 'search:"%s"'%sr
else:
sr = db.prefs.get('cs_virtual_lib_on_startup', '')
if sr:
if sr not in virt_libs:
prints('WARNING: Content server: virtual library ',
sr, ' does not exist')
sr = ''
else:
sr = virt_libs[sr]
self.search_restriction = sr
self.reset_caches()
def graceful(self): def graceful(self):
cherrypy.engine.graceful() cherrypy.engine.graceful()
def set_search_restriction(self, restriction):
self.search_restriction_name = restriction
if restriction:
if restriction not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
restriction, ' does not exist')
self.search_restriction = ''
else:
self.search_restriction = 'search:"%s"'%restriction
else:
self.search_restriction = ''
self.reset_caches()
def setup_loggers(self): def setup_loggers(self):
access_file = log_access_file access_file = log_access_file
error_file = log_error_file error_file = log_error_file

View File

@ -145,10 +145,7 @@ def render_rating(rating, url_prefix, container='span', prefix=None): # {{{
# }}} # }}}
def get_category_items(category, items, restriction, datatype, prefix): # {{{ def get_category_items(category, items, datatype, prefix): # {{{
if category == 'search':
items = [x for x in items if x.name != restriction]
def item(i): def item(i):
templ = (u'<div title="{4}" class="category-item">' templ = (u'<div title="{4}" class="category-item">'
@ -489,8 +486,7 @@ class BrowseServer(object):
if not cats and len(items) == 1: if not cats and len(items) == 1:
# Only one item in category, go directly to book list # Only one item in category, go directly to book list
html = get_category_items(category, items, html = get_category_items(category, items,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
href = re.search(r'<a href="([^"]+)"', html) href = re.search(r'<a href="([^"]+)"', html)
if href is not None: if href is not None:
raise cherrypy.HTTPRedirect(href.group(1)) raise cherrypy.HTTPRedirect(href.group(1))
@ -498,8 +494,7 @@ class BrowseServer(object):
if len(items) <= self.opts.max_opds_ungrouped_items: if len(items) <= self.opts.max_opds_ungrouped_items:
script = 'false' script = 'false'
items = get_category_items(category, items, items = get_category_items(category, items,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
else: else:
getter = lambda x: unicode(getattr(x, 'sort', x.name)) getter = lambda x: unicode(getattr(x, 'sort', x.name))
starts = set([]) starts = set([])
@ -588,8 +583,7 @@ class BrowseServer(object):
sort = self.browse_sort_categories(entries, sort) sort = self.browse_sort_categories(entries, sort)
entries = get_category_items(category, entries, entries = get_category_items(category, entries,
self.search_restriction_name, datatype, datatype, self.opts.url_prefix)
self.opts.url_prefix)
return json.dumps(entries, ensure_ascii=True) return json.dumps(entries, ensure_ascii=True)

View File

@ -55,10 +55,11 @@ The OPDS interface is advertised via BonJour automatically.
help=_('Write process PID to the specified file')) help=_('Write process PID to the specified file'))
parser.add_option('--daemonize', default=False, action='store_true', parser.add_option('--daemonize', default=False, action='store_true',
help='Run process in background as a daemon. No effect on windows.') help='Run process in background as a daemon. No effect on windows.')
parser.add_option('--restriction', default=None, parser.add_option('--restriction', '--virtual-library', default=None,
help=_('Specifies a restriction to be used for this invocation. ' help=_('Specifies a virtual library to be used for this invocation. '
'This option overrides any per-library settings specified' 'This option overrides any per-library settings specified'
' in the GUI')) ' in the GUI. For compatibility, if the value is not a '
'virtual library but is a saved search, that saved search is used.'))
parser.add_option('--auto-reload', default=False, action='store_true', parser.add_option('--auto-reload', default=False, action='store_true',
help=_('Auto reload server when source code changes. May not' help=_('Auto reload server when source code changes. May not'
' work in all environments.')) ' work in all environments.'))