diff --git a/manual/gui.rst b/manual/gui.rst index bcce1e4d37..7b3479e088 100644 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -359,7 +359,7 @@ You can build advanced search queries easily using the :guilabel:`Advanced searc clicking the button |sbi|. Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover, -comments, format, identifiers, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field becomes available, when searching the calibre library view. To find the search name (actually called the `lookup name`) for a custom column, hover your mouse over the column header in the library view. +comments, format, identifiers, date, pubdate, search, size, vl`` and custom columns. If a device is plugged in, the ``ondevice`` field becomes available, when searching the calibre library view. To find the search name (actually called the `lookup name`) for a custom column, hover your mouse over the column header in the library view. The syntax for searching for dates is:: @@ -400,6 +400,12 @@ The special field ``search`` is used for saved searches. So if you save a search "My spouse's books" you can enter ``search:"My spouse's books"`` in the Search bar to reuse the saved search. More about saving searches below. +The special field ``vl`` is used to search for books in a virtual library. For +example, ``vl:Read`` will find all the books in the *Read* virtual library. The search +``vl:Read and vl:"Science Fiction"`` will find all the books that are in both the *Read* and +*Science Fiction* virtual libraries. The value following ``vl:`` must be the name of a +virtual library. If the virtual library name contains spaces then surround it with quotes. + You can search for the absence or presence of a field using the special "true" and "false" values. For example:: cover:false will give you all books without a cover diff --git a/manual/virtual_libraries.rst b/manual/virtual_libraries.rst index 51f378bdb7..d3138d6ced 100644 --- a/manual/virtual_libraries.rst +++ b/manual/virtual_libraries.rst @@ -95,6 +95,23 @@ virtual libraries as tabs`. You can re-arrange the tabs by drag and drop and close ones you do not want to see. Closed tabs can be restored by right-clicking on the tab bar. +Using Virtual libraries in searches +------------------------------------- + +You can search for books that are in a virtual library using the ``vl:`` prefix. For +example, ``vl:Read`` will find all the books in the *Read* virtual library. The search +``vl:Read and vl:"Science Fiction"`` will find all the books that are in both the *Read* and +*Science Fiction* virtual libraries. + +The value following ``vl:`` must be the name of a virtual library. If the virtual library name +contains spaces then surround it with quotes. + +One use for a virtual library search is in the content server. In +:guilabel:`Preferences->Sharing over the net->Require username/password` you +can limit the calibre libraries visible to a user. For each visible library you +can specify a search expression to further limit which books are seen. Use +``vl:"Virtual library name"`` to limit the books to those in a virtual library. + Using additional restrictions ------------------------------- diff --git a/src/calibre/db/search.py b/src/calibre/db/search.py index 71138d0132..947429d2de 100644 --- a/src/calibre/db/search.py +++ b/src/calibre/db/search.py @@ -498,6 +498,15 @@ class Parser(SearchQueryParser): # {{{ if location not in self.all_search_locations: return matches + if location == 'vl': + vl = self.dbcache._pref('virtual_libraries', {}).get(query) if query else None + if not vl: + raise ParseException(_('No such virtual library: {}').format(query)) + try: + return candidates & self.dbcache.books_in_virtual_library(query) + except RuntimeError: + raise ParseException(_('Virtual library search is recursive: {}').format(query)) + if (len(location) > 2 and location.startswith('@') and location[1:] in self.grouped_search_terms): location = location[1:] @@ -915,4 +924,3 @@ class Search(object): self.cache.add(query, result) return result - diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 66153ddce5..12b35e0130 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -380,7 +380,7 @@ class FieldMetadata(object): 'int', 'float', 'bool', 'series', 'composite', 'enumeration']) # search labels that are not db columns - search_items = ['all', 'search'] + search_items = ['all', 'search', 'vl'] __calibre_serializable__ = True def __init__(self): diff --git a/src/calibre/srv/errors.py b/src/calibre/srv/errors.py index 676a69083c..db251b34cf 100644 --- a/src/calibre/srv/errors.py +++ b/src/calibre/srv/errors.py @@ -58,6 +58,12 @@ class HTTPForbidden(HTTPSimpleResponse): HTTPSimpleResponse.__init__(self, httplib.FORBIDDEN, http_message, close_connection, log=log) +class HTTPInternalServerError(HTTPSimpleResponse): + + def __init__(self, http_message='', close_connection=True, log=None): + HTTPSimpleResponse.__init__(self, httplib.INTERNAL_SERVER_ERROR, http_message, close_connection, log=log) + + class BookNotFound(HTTPNotFound): def __init__(self, book_id, db): diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 12a33e4735..421d313342 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -118,8 +118,10 @@ class Context(object): raise return frozenset() - def get_categories(self, request_data, db, sort='name', first_letter_sort=True, vl=''): - restrict_to_ids = self.get_effective_book_ids(db, request_data, vl) + def get_categories(self, request_data, db, sort='name', first_letter_sort=True, + vl='', report_parse_errors=False): + restrict_to_ids = self.get_effective_book_ids(db, request_data, vl, + report_parse_errors=report_parse_errors) key = restrict_to_ids, sort, first_letter_sort with self.lock: cache = self.library_broker.category_caches[db.server_library_id] diff --git a/src/calibre/srv/opds.py b/src/calibre/srv/opds.py index 35e3868666..ae14025fea 100644 --- a/src/calibre/srv/opds.py +++ b/src/calibre/srv/opds.py @@ -20,8 +20,9 @@ from calibre.library.comments import comments_to_html from calibre import guess_type, prepare_string_for_xml as xml from calibre.utils.icu import sort_key from calibre.utils.date import as_utc, timestampfromdt, is_date_undefined +from calibre.utils.search_query_parser import ParseException -from calibre.srv.errors import HTTPNotFound +from calibre.srv.errors import HTTPNotFound, HTTPInternalServerError from calibre.srv.routes import endpoint from calibre.srv.utils import get_library_data, http_date, Offsets @@ -381,8 +382,9 @@ class RequestContext(object): def last_modified(self): return self.db.last_modified() - def get_categories(self): - return self.ctx.get_categories(self.rd, self.db) + def get_categories(self, report_parse_errors=False): + return self.ctx.get_categories(self.rd, self.db, + report_parse_errors=report_parse_errors) def search(self, query): return self.ctx.search(self.rd, self.db, query) @@ -470,7 +472,11 @@ def get_navcatalog(request_context, which, page_url, up_url, offset=0): def opds(ctx, rd): rc = RequestContext(ctx, rd) db = rc.db - categories = rc.get_categories() + try: + categories = rc.get_categories(report_parse_errors=True) + except ParseException as p: + raise HTTPInternalServerError(p.msg) + category_meta = db.field_metadata cats = [ (_('Newest'), _('Date'), 'Onewest'),