%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