%PDF- %PDF-
Direktori : /lib/calibre/calibre/db/ |
Current File : //lib/calibre/calibre/db/lazy.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' import weakref from functools import wraps from collections.abc import MutableMapping, MutableSequence from copy import deepcopy from calibre.ebooks.metadata.book.base import Metadata, SIMPLE_GET, TOP_LEVEL_IDENTIFIERS, NULL_VALUES, ALL_METADATA_FIELDS from calibre.ebooks.metadata.book.formatter import SafeFormat from calibre.utils.date import utcnow from polyglot.builtins import native_string_type # Lazy format metadata retrieval {{{ ''' Avoid doing stats on all files in a book when getting metadata for that book. Speeds up calibre startup with large libraries/libraries on a network share, with a composite custom column. ''' def resolved(f): @wraps(f) def wrapper(self, *args, **kwargs): if getattr(self, '_must_resolve', True): self._resolve() self._must_resolve = False return f(self, *args, **kwargs) return wrapper class MutableBase: @resolved def __str__(self): return native_string_type(self._values) @resolved def __repr__(self): return repr(self._values) @resolved def __unicode__(self): return str(self._values) @resolved def __len__(self): return len(self._values) @resolved def __iter__(self): return iter(self._values) @resolved def __contains__(self, key): return key in self._values @resolved def __getitem__(self, fmt): return self._values[fmt] @resolved def __setitem__(self, key, val): self._values[key] = val @resolved def __delitem__(self, key): del self._values[key] class FormatMetadata(MutableBase, MutableMapping): def __init__(self, db, id_, formats): self._dbwref = weakref.ref(db) self._id = id_ self._formats = formats def _resolve(self): db = self._dbwref() self._values = {} for f in self._formats: try: self._values[f] = db.format_metadata(self._id, f) except: pass class FormatsList(MutableBase, MutableSequence): def __init__(self, formats, format_metadata): self._formats = formats self._format_metadata = format_metadata def _resolve(self): self._values = [f for f in self._formats if f in self._format_metadata] @resolved def insert(self, idx, val): self._values.insert(idx, val) # }}} # Lazy metadata getters {{{ ga = object.__getattribute__ sa = object.__setattr__ def simple_getter(field, default_value=None): def func(dbref, book_id, cache): try: return cache[field] except KeyError: db = dbref() cache[field] = ret = db.field_for(field, book_id, default_value=default_value) return ret return func def pp_getter(field, postprocess, default_value=None): def func(dbref, book_id, cache): try: return cache[field] except KeyError: db = dbref() cache[field] = ret = postprocess(db.field_for(field, book_id, default_value=default_value)) return ret return func def adata_getter(field): def func(dbref, book_id, cache): try: author_ids, adata = cache['adata'] except KeyError: db = dbref() with db.safe_read_lock: author_ids = db._field_ids_for('authors', book_id) adata = db._author_data(author_ids) cache['adata'] = (author_ids, adata) k = 'sort' if field == 'author_sort_map' else 'link' return {adata[i]['name']:adata[i][k] for i in author_ids} return func def dt_getter(field): def func(dbref, book_id, cache): try: return cache[field] except KeyError: db = dbref() cache[field] = ret = db.field_for(field, book_id, default_value=utcnow()) return ret return func def item_getter(field, default_value=None, key=0): def func(dbref, book_id, cache): try: return cache[field] except KeyError: db = dbref() ret = cache[field] = db.field_for(field, book_id, default_value=default_value) try: return ret[key] except (IndexError, KeyError): return default_value return func def fmt_getter(field): def func(dbref, book_id, cache): try: format_metadata = cache['format_metadata'] except KeyError: db = dbref() format_metadata = {} for fmt in db.formats(book_id, verify_formats=False): m = db.format_metadata(book_id, fmt) if m: format_metadata[fmt] = m if field == 'formats': return sorted(format_metadata) or None return format_metadata return func def approx_fmts_getter(dbref, book_id, cache): try: return cache['formats'] except KeyError: db = dbref() cache['formats'] = ret = list(db.field_for('formats', book_id)) return ret def series_index_getter(field='series'): def func(dbref, book_id, cache): try: series = getters[field](dbref, book_id, cache) except KeyError: series = custom_getter(field, dbref, book_id, cache) if series: try: return cache[field + '_index'] except KeyError: db = dbref() cache[field + '_index'] = ret = db.field_for(field + '_index', book_id, default_value=1.0) return ret return func def has_cover_getter(dbref, book_id, cache): try: return cache['has_cover'] except KeyError: db = dbref() cache['has_cover'] = ret = _('Yes') if db.field_for('cover', book_id, default_value=False) else '' return ret fmt_custom = lambda x:list(x) if isinstance(x, tuple) else x def custom_getter(field, dbref, book_id, cache): try: return cache[field] except KeyError: db = dbref() cache[field] = ret = fmt_custom(db.field_for(field, book_id)) return ret def composite_getter(mi, field, dbref, book_id, cache, formatter, template_cache): try: return cache[field] except KeyError: cache[field] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field try: db = dbref() with db.safe_read_lock: try: fo = db.fields[field] except KeyError: ret = cache[field] = _('Invalid field: %s') % field else: ret = cache[field] = fo._render_composite_with_cache(book_id, mi, formatter, template_cache) except Exception: import traceback traceback.print_exc() return 'ERROR WHILE EVALUATING: %s' % field return ret def virtual_libraries_getter(dbref, book_id, cache): ''' This method is deprecated because it doesn't (and can't) return virtual library names when the VL search references marked books. It is replaced by db.view.get_virtual_libraries_for_books() ''' try: return cache['virtual_libraries'] except KeyError: db = dbref() vls = db.virtual_libraries_for_books((book_id,))[book_id] 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')), 'authors':pp_getter('authors', list, (_('Unknown'),)), 'author_sort':simple_getter('author_sort', _('Unknown')), 'uuid':simple_getter('uuid', 'dummy'), 'book_size':simple_getter('size', 0), 'ondevice_col':simple_getter('ondevice', ''), 'languages':pp_getter('languages', list), 'language':item_getter('languages', default_value=NULL_VALUES['language']), 'db_approx_formats': approx_fmts_getter, 'has_cover': has_cover_getter, 'tags':pp_getter('tags', list, (_('Unknown'),)), 'series_index':series_index_getter(), 'application_id':lambda x, book_id, y: book_id, 'id':lambda x, book_id, y: book_id, 'virtual_libraries':virtual_libraries_getter, } for field in ('comments', 'publisher', 'identifiers', 'series', 'rating'): getters[field] = simple_getter(field) for field in ('author_sort_map', 'author_link_map'): getters[field] = adata_getter(field) for field in ('timestamp', 'pubdate', 'last_modified'): getters[field] = dt_getter(field) for field in TOP_LEVEL_IDENTIFIERS: getters[field] = item_getter('identifiers', key=field) for field in ('formats', 'format_metadata'): getters[field] = fmt_getter(field) # }}} class ProxyMetadata(Metadata): def __init__(self, db, book_id, formatter=None): sa(self, 'template_cache', db.formatter_template_cache) 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', {'cover_data':(None,None), 'device_collections':[]}) sa(self, '_user_metadata', db.field_metadata) def __getattribute__(self, field): getter = getters.get(field, None) 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) except AttributeError: pass um = ga(self, '_user_metadata') d = um.get(field, None) if d is not None: dt = d['datatype'] if dt != 'composite': if field.endswith('_index') and dt == 'float': return series_index_getter(field[:-6])(ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache')) return custom_getter(field, ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache')) return composite_getter(self, field, ga(self, '_db'), ga(self, '_book_id'), ga(self, '_cache'), ga(self, 'formatter'), ga(self, 'template_cache')) try: return ga(self, '_cache')[field] except KeyError: raise AttributeError('Metadata object has no attribute named: %r' % field) def __setattr__(self, field, val, extra=None): cache = ga(self, '_cache') cache[field] = val if extra is not None: cache[field + '_index'] = val def get_user_metadata(self, field, make_copy=False): um = ga(self, '_user_metadata') try: ans = um[field] except KeyError: pass else: if make_copy: ans = deepcopy(ans) return ans def get_extra(self, field, default=None): um = ga(self, '_user_metadata') if field + '_index' in um: try: return getattr(self, field + '_index') except AttributeError: return default raise AttributeError( 'Metadata object has no attribute named: '+ repr(field)) def custom_field_keys(self): um = ga(self, '_user_metadata') return iter(um.custom_field_keys()) def get_standard_metadata(self, field, make_copy=False): field_metadata = ga(self, '_user_metadata') if field in field_metadata and field_metadata[field]['kind'] == 'field': if make_copy: return deepcopy(field_metadata[field]) return field_metadata[field] return None def all_field_keys(self): um = ga(self, '_user_metadata') return frozenset(ALL_METADATA_FIELDS.union(frozenset(um))) @property def _proxy_metadata(self): return self