%PDF- %PDF-
| Direktori : /lib/calibre/calibre/db/ |
| Current File : //lib/calibre/calibre/db/view.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import weakref, operator, numbers
from functools import partial
from polyglot.builtins import iteritems, itervalues
from calibre.ebooks.metadata import title_sort
from calibre.utils.config_base import tweaks, prefs
from calibre.db.write import uniq
def sanitize_sort_field_name(field_metadata, field):
field = field_metadata.search_term_to_field_key(field.lower().strip())
# translate some fields to their hidden equivalent
field = {'title': 'sort', 'authors':'author_sort'}.get(field, field)
return field
class MarkedVirtualField:
def __init__(self, marked_ids):
self.marked_ids = marked_ids
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
for book_id in candidates:
yield self.marked_ids.get(book_id, default_value), {book_id}
def sort_keys_for_books(self, get_metadata, lang_map):
g = self.marked_ids.get
return lambda book_id:g(book_id, '')
class TableRow:
def __init__(self, book_id, view):
self.book_id = book_id
self.view = weakref.ref(view)
self.column_count = view.column_count
def __getitem__(self, obj):
view = self.view()
if isinstance(obj, slice):
return [view._field_getters[c](self.book_id)
for c in range(*obj.indices(len(view._field_getters)))]
else:
return view._field_getters[obj](self.book_id)
def __len__(self):
return self.column_count
def __iter__(self):
for i in range(self.column_count):
yield self[i]
def format_is_multiple(x, sep=',', repl=None):
if not x:
return None
if repl is not None:
x = (y.replace(sep, repl) for y in x)
return sep.join(x)
def format_identifiers(x):
if not x:
return None
return ','.join('%s:%s'%(k, v) for k, v in iteritems(x))
class View:
''' A table view of the database, with rows and columns. Also supports
filtering and sorting. '''
def __init__(self, cache):
self.cache = cache
self.marked_ids = {}
self.marked_listeners = {}
self.search_restriction_book_count = 0
self.search_restriction = self.base_restriction = ''
self.search_restriction_name = self.base_restriction_name = ''
self._field_getters = {}
self.column_count = len(cache.backend.FIELD_MAP)
for col, idx in iteritems(cache.backend.FIELD_MAP):
label, fmt = col, lambda x:x
func = {
'id': self._get_id,
'au_map': self.get_author_data,
'ondevice': self.get_ondevice,
'marked': self.get_marked,
'series_sort':self.get_series_sort,
}.get(col, self._get)
if isinstance(col, numbers.Integral):
label = self.cache.backend.custom_column_num_map[col]['label']
label = (self.cache.backend.field_metadata.custom_field_prefix + label)
if label.endswith('_index'):
try:
num = int(label.partition('_')[0])
except ValueError:
pass # series_index
else:
label = self.cache.backend.custom_column_num_map[num]['label']
label = (self.cache.backend.field_metadata.custom_field_prefix + label + '_index')
fm = self.field_metadata[label]
fm
if label == 'authors':
fmt = partial(format_is_multiple, repl='|')
elif label in {'tags', 'languages', 'formats'}:
fmt = format_is_multiple
elif label == 'cover':
fmt = bool
elif label == 'identifiers':
fmt = format_identifiers
elif fm['datatype'] == 'text' and fm['is_multiple']:
sep = fm['is_multiple']['cache_to_list']
if sep not in {'&','|'}:
sep = '|'
fmt = partial(format_is_multiple, sep=sep)
self._field_getters[idx] = partial(func, label, fmt=fmt) if func == self._get else func
self._map = tuple(sorted(self.cache.all_book_ids()))
self._map_filtered = tuple(self._map)
self.full_map_is_sorted = True
self.sort_history = [('id', True)]
def add_marked_listener(self, func):
self.marked_listeners[id(func)] = weakref.ref(func)
def add_to_sort_history(self, items):
self.sort_history = uniq((list(items) + list(self.sort_history)),
operator.itemgetter(0))[:tweaks['maximum_resort_levels']]
def count(self):
return len(self._map)
def get_property(self, id_or_index, index_is_id=False, loc=-1):
book_id = id_or_index if index_is_id else self._map_filtered[id_or_index]
return self._field_getters[loc](book_id)
def sanitize_sort_field_name(self, field):
return sanitize_sort_field_name(self.field_metadata, field)
@property
def field_metadata(self):
return self.cache.field_metadata
def _get_id(self, idx, index_is_id=True):
if index_is_id and not self.cache.has_id(idx):
raise IndexError('No book with id %s present'%idx)
return idx if index_is_id else self.index_to_id(idx)
def has_id(self, book_id):
return self.cache.has_id(book_id)
def __getitem__(self, row):
return TableRow(self._map_filtered[row], self)
def __len__(self):
return len(self._map_filtered)
def __iter__(self):
for book_id in self._map_filtered:
yield TableRow(book_id, self)
def iterall(self):
for book_id in self.iterallids():
yield TableRow(book_id, self)
def iterallids(self):
yield from sorted(self._map)
def tablerow_for_id(self, book_id):
return TableRow(book_id, self)
def get_field_map_field(self, row, col, index_is_id=True):
'''
Supports the legacy FIELD_MAP interface for getting metadata. Do not use
in new code.
'''
getter = self._field_getters[col]
return getter(row, index_is_id=index_is_id)
def index_to_id(self, idx):
return self._map_filtered[idx]
def id_to_index(self, book_id):
return self._map_filtered.index(book_id)
row = index_to_id
def index(self, book_id, cache=False):
x = self._map if cache else self._map_filtered
return x.index(book_id)
def _get(self, field, idx, index_is_id=True, default_value=None, fmt=lambda x:x):
id_ = idx if index_is_id else self.index_to_id(idx)
if index_is_id and not self.cache.has_id(id_):
raise IndexError('No book with id %s present'%idx)
return fmt(self.cache.field_for(field, id_, default_value=default_value))
def get_series_sort(self, idx, index_is_id=True, default_value=''):
book_id = idx if index_is_id else self.index_to_id(idx)
with self.cache.safe_read_lock:
lang_map = self.cache.fields['languages'].book_value_map
lang = lang_map.get(book_id, None) or None
if lang:
lang = lang[0]
return title_sort(self.cache._field_for('series', book_id, default_value=''),
order=tweaks['title_series_sorting'], lang=lang)
def get_ondevice(self, idx, index_is_id=True, default_value=''):
id_ = idx if index_is_id else self.index_to_id(idx)
return self.cache.field_for('ondevice', id_, default_value=default_value)
def get_marked(self, idx, index_is_id=True, default_value=None):
id_ = idx if index_is_id else self.index_to_id(idx)
return self.marked_ids.get(id_, default_value)
def get_author_data(self, idx, index_is_id=True, default_value=None):
id_ = idx if index_is_id else self.index_to_id(idx)
with self.cache.safe_read_lock:
ids = self.cache._field_ids_for('authors', id_)
adata = self.cache._author_data(ids)
ans = [':::'.join((adata[aid]['name'], adata[aid]['sort'], adata[aid]['link'])) for aid in ids if aid in adata]
return ':#:'.join(ans) if ans else default_value
def get_virtual_libraries_for_books(self, ids):
return self.cache.virtual_libraries_for_books(
ids, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)})
def _do_sort(self, ids_to_sort, fields=(), subsort=False):
fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields]
keys = self.field_metadata.sortable_field_keys()
fields = [x for x in fields if x[0] in keys]
if subsort and 'sort' not in [x[0] for x in fields]:
fields += [('sort', True)]
if not fields:
fields = [('timestamp', False)]
return self.cache.multisort(
fields, ids_to_sort=ids_to_sort,
virtual_fields={'marked':MarkedVirtualField(self.marked_ids)})
def multisort(self, fields=[], subsort=False, only_ids=None):
sorted_book_ids = self._do_sort(self._map if only_ids is None else only_ids, fields=fields, subsort=subsort)
if only_ids is None:
self._map = tuple(sorted_book_ids)
self.full_map_is_sorted = True
self.add_to_sort_history(fields)
if len(self._map_filtered) == len(self._map):
self._map_filtered = tuple(self._map)
else:
fids = frozenset(self._map_filtered)
self._map_filtered = tuple(i for i in self._map if i in fids)
else:
smap = {book_id:i for i, book_id in enumerate(sorted_book_ids)}
only_ids.sort(key=smap.get)
def incremental_sort(self, fields=(), subsort=False):
if len(self._map) == len(self._map_filtered):
return self.multisort(fields=fields, subsort=subsort)
self._map_filtered = tuple(self._do_sort(self._map_filtered, fields=fields, subsort=subsort))
self.full_map_is_sorted = False
self.add_to_sort_history(fields)
def search(self, query, return_matches=False, sort_results=True):
ans = self.search_getting_ids(query, self.search_restriction,
set_restriction_count=True, sort_results=sort_results)
if return_matches:
return ans
self._map_filtered = tuple(ans)
def _build_restriction_string(self, restriction):
if self.base_restriction:
if restriction:
return f'({self.base_restriction}) and ({restriction})'
else:
return self.base_restriction
else:
return restriction
def search_getting_ids(self, query, search_restriction,
set_restriction_count=False, use_virtual_library=True, sort_results=True):
if use_virtual_library:
search_restriction = self._build_restriction_string(search_restriction)
q = ''
if not query or not query.strip():
q = search_restriction
else:
q = query
if search_restriction:
q = f'({search_restriction}) and ({query})'
if not q:
if set_restriction_count:
self.search_restriction_book_count = len(self._map)
rv = list(self._map)
if sort_results and not self.full_map_is_sorted:
rv = self._do_sort(rv, fields=self.sort_history)
self._map = tuple(rv)
self.full_map_is_sorted = True
return rv
matches = self.cache.search(
query, search_restriction, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)})
if len(matches) == len(self._map):
rv = list(self._map)
else:
rv = [x for x in self._map if x in matches]
if sort_results and not self.full_map_is_sorted:
# We need to sort the search results
if matches.issubset(frozenset(self._map_filtered)):
rv = [x for x in self._map_filtered if x in matches]
else:
rv = self._do_sort(rv, fields=self.sort_history)
if len(matches) == len(self._map):
# We have sorted all ids, update self._map
self._map = tuple(rv)
self.full_map_is_sorted = True
if set_restriction_count and q == search_restriction:
self.search_restriction_book_count = len(rv)
return rv
def get_search_restriction(self):
return self.search_restriction
def set_search_restriction(self, 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):
return bool(self.search_restriction) or bool(self.base_restriction)
def get_search_restriction_book_count(self):
return self.search_restriction_book_count
def change_search_locations(self, newlocs):
self.cache.change_search_locations(newlocs)
def set_marked_ids(self, id_dict):
'''
ids in id_dict are "marked". They can be searched for by
using the search term ``marked:true``. Pass in an empty dictionary or
set to clear marked ids.
:param id_dict: Either a dictionary mapping ids to values or a set
of ids. In the latter case, the value is set to 'true' for all ids. If
a mapping is provided, then the search can be used to search for
particular values: ``marked:value``
'''
old_marked_ids = set(self.marked_ids)
if not hasattr(id_dict, 'items'):
# Simple list. Make it a dict of string 'true'
self.marked_ids = dict.fromkeys(id_dict, 'true')
else:
# Ensure that all the items in the dict are text
self.marked_ids = {k: str(v) for k, v in iteritems(id_dict)}
# This invalidates all searches in the cache even though the cache may
# be shared by multiple views. This is not ideal, but...
cmids = set(self.marked_ids)
changed_ids = old_marked_ids | cmids
self.cache.clear_search_caches(changed_ids)
self.cache.clear_caches(book_ids=changed_ids)
if old_marked_ids != cmids:
for funcref in itervalues(self.marked_listeners):
func = funcref()
if func is not None:
func(old_marked_ids, cmids)
def toggle_marked_ids(self, book_ids):
book_ids = set(book_ids)
mids = set(self.marked_ids)
common = mids.intersection(book_ids)
self.set_marked_ids((mids | book_ids) - common)
def refresh(self, field=None, ascending=True, clear_caches=True, do_search=True):
self._map = tuple(sorted(self.cache.all_book_ids()))
self._map_filtered = tuple(self._map)
self.full_map_is_sorted = True
self.sort_history = [('id', True)]
if clear_caches:
self.cache.clear_caches()
if field is not None:
self.sort(field, ascending)
if do_search and (self.search_restriction or self.base_restriction):
self.search('', return_matches=False)
def refresh_ids(self, ids):
self.cache.clear_caches(book_ids=ids)
try:
return list(map(self.id_to_index, ids))
except ValueError:
pass
return None
def remove(self, book_id):
try:
self._map = tuple(bid for bid in self._map if bid != book_id)
except ValueError:
pass
try:
self._map_filtered = tuple(bid for bid in self._map_filtered if bid != book_id)
except ValueError:
pass
def books_deleted(self, ids):
for book_id in ids:
self.remove(book_id)
def books_added(self, ids):
ids = tuple(ids)
self._map = ids + self._map
self._map_filtered = ids + self._map_filtered
if prefs['mark_new_books']:
self.toggle_marked_ids(ids)