diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index fd20ae8c9b..4cdbb633c9 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1901,6 +1901,41 @@ class Cache(object): ans[book].append(lib) return {k:tuple(sorted(v, key=sort_key)) for k, v in ans.iteritems()} + @read_api + def user_categories_for_books(self, book_ids, proxy_metadata_map=None): + ''' Return the user categories for the specified books. + proxy_metadata_map is optional and is useful for a performance boost, + in contexts where a ProxyMetadata object for the books already exists. + It should be a mapping of book_ids to their corresponding ProxyMetadata + objects. + ''' + user_cats = self.backend.prefs['user_categories'] + pmm = proxy_metadata_map or {} + ans = {} + + for book_id in book_ids: + user_cat_vals = ans[book_id] = {} + for ucat, categories in user_cats.iteritems(): + user_cat_vals[ucat] = res = [] + proxy_metadata = pmm.get(book_id) or self._get_proxy_metadata(book_id) + for name, cat, ign in categories: + try: + field_obj = self.fields[cat] + except KeyError: + continue + + if field_obj.is_composite: + v = field_obj.get_value_with_cache(book_id, lambda x:proxy_metadata) + else: + v = self._fast_field_for(field_obj, book_id) + + if isinstance(v, (list, tuple)): + if name in v: + res.append([name, cat]) + elif name == v: + res.append([name, cat]) + return ans + @write_api def embed_metadata(self, book_ids, only_fmts=None, report_error=None, report_progress=None): ''' Update metadata in all formats of the specified book_ids to current metadata in the database. ''' diff --git a/src/calibre/db/lazy.py b/src/calibre/db/lazy.py index fbe37713d5..ed174e9309 100644 --- a/src/calibre/db/lazy.py +++ b/src/calibre/db/lazy.py @@ -241,6 +241,16 @@ def virtual_libraries_getter(dbref, book_id, cache): ret = cache['virtual_libraries'] = ', '.join(vls) return ret +def user_categories_getter(proxy_metadata): + cache = ga(proxy_metadata, '_cache') + try: + return cache['user_categories'] + except KeyError: + db = ga(proxy_metadata, '_db')() + book_id = ga(proxy_metadata, '_book_id') + ret = cache['user_categories'] = db.user_categories_for_books((book_id,), {book_id:proxy_metadata})[book_id] + return ret + getters = { 'title':simple_getter('title', _('Unknown')), 'title_sort':simple_getter('sort', _('Unknown')), @@ -283,7 +293,7 @@ class ProxyMetadata(Metadata): sa(self, 'formatter', SafeFormat() if formatter is None else formatter) sa(self, '_db', weakref.ref(db)) sa(self, '_book_id', book_id) - sa(self, '_cache', {'user_categories':{}, 'cover_data':(None,None), 'device_collections':[]}) + sa(self, '_cache', {'cover_data':(None,None), 'device_collections':[]}) sa(self, '_user_metadata', db.field_metadata) def __getattribute__(self, field): @@ -291,6 +301,8 @@ class ProxyMetadata(Metadata): if getter is not None: return getter(ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache')) if field in SIMPLE_GET: + if field == 'user_categories': + return user_categories_getter(self) return ga(self, '_cache').get(field, None) try: return ga(self, field) diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index aa82e7a9e3..44d9841305 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -527,7 +527,7 @@ class ReadingTest(BaseTest): from calibre.ebooks.metadata.book.base import STANDARD_METADATA_FIELDS cache = self.init_cache() for book_id in cache.all_book_ids(): - mi = cache.get_metadata(book_id, get_user_categories=False) + mi = cache.get_metadata(book_id, get_user_categories=True) pmi = cache.get_proxy_metadata(book_id) self.assertSetEqual(set(mi.custom_field_keys()), set(pmi.custom_field_keys())) diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 551870a330..42f06395c2 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -1397,6 +1397,23 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction): return mi._proxy_metadata.virtual_libraries return _('This function can be used only in the GUI') +class BuiltinUserCategories(BuiltinFormatterFunction): + name = 'user_categories' + arg_count = 0 + category = 'Get values from metadata' + __doc__ = doc = _('user_categories() -- return a comma-separated list of ' + 'the user categories that contain this book. This function ' + 'works only in the GUI. If you want to use these values ' + 'in save-to-disk or send-to-device templates then you ' + 'must make a custom "Column built from other columns", use ' + 'the function in that column\'s template, and use that ' + 'column\'s value in your save/send templates') + + def evaluate(self, formatter, kwargs, mi, locals_): + if hasattr(mi, '_proxy_metadata'): + return ', '.join(k for k, v in mi._proxy_metadata.user_categories.iteritems() if v) + return _('This function can be used only in the GUI') + class BuiltinTransliterate(BuiltinFormatterFunction): name = 'transliterate' arg_count = 1 @@ -1480,7 +1497,7 @@ _formatter_builtins = [ BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundComma(), BuiltinSwitch(), BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(), BuiltinToday(), BuiltinTransliterate(), BuiltinUppercase(), - BuiltinVirtualLibraries() + BuiltinUserCategories(), BuiltinVirtualLibraries() ] class FormatterUserFunction(FormatterFunction):