diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 7b8eb07908..3d6d6b1bb8 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -138,20 +138,66 @@ class Metadata(object): def set(self, field, val, extra=None): self.__setattr__(field, val, extra) - @property - def all_keys(self): + # field-oriented interface. Intended to be the same as in LibraryDatabase + + def standard_field_keys(self): ''' - All attribute keys known by this instance, even if their value is None + return a list of all possible keys, even if this book doesn't have them + ''' + return STANDARD_METADATA_FIELDS + + def custom_field_keys(self): + ''' + return a list of the custom fields in this book + ''' + return object.__getattribute__(self, '_data')['user_metadata'].iterkeys() + + def all_field_keys(self): + ''' + All field keys known by this instance, even if their value is None ''' _data = object.__getattribute__(self, '_data') return frozenset(ALL_METADATA_FIELDS.union(_data['user_metadata'].iterkeys())) - @property + def metadata_for_field(self, key): + ''' + return metadata describing a standard or custom field. + ''' + if key in self.user_metadata_keys(): + return self.get_standard_metadata(self, key, make_copy=False) + return self.get_user_metadata(key, make_copy=False) + def user_metadata_keys(self): - 'The set of user metadata names this object knows about' + ''' + Return the standard keys actually in this book. + ''' _data = object.__getattribute__(self, '_data') return frozenset(_data['user_metadata'].iterkeys()) + def all_non_none_fields(self): + ''' + Return a dictionary containing all non-None metadata fields, including + the custom ones. + ''' + result = {} + _data = object.__getattribute__(self, '_data') + for attr in STANDARD_METADATA_FIELDS: + v = _data.get(attr, None) + if v is not None: + result[attr] = v + for attr in _data['user_metadata'].iterkeys(): + v = _data['user_metadata'][attr]['#value#'] + if v is not None: + result[attr] = v + if _data['user_metadata'][attr]['datatype'] == 'series': + result[attr+'_index'] = _data['user_metadata'][attr]['#extra#'] + return result + + # End of field-oriented interface + + # Extended interfaces. These permit one to get copies of metadata dictionaries, and to + # get and set custom field metadata + def get_standard_metadata(self, field, make_copy): ''' return field metadata from the field if it is there. Otherwise return @@ -237,30 +283,11 @@ class Metadata(object): _data = object.__getattribute__(self, '_data') _data['user_metadata'][field] = metadata - def get_all_non_none_attributes(self): - ''' - Return a dictionary containing all non-None metadata fields, including - the custom ones. - ''' - result = {} - _data = object.__getattribute__(self, '_data') - for attr in STANDARD_METADATA_FIELDS: - v = _data.get(attr, None) - if v is not None: - result[attr] = v - for attr in _data['user_metadata'].iterkeys(): - v = _data['user_metadata'][attr]['#value#'] - if v is not None: - result[attr] = v - if _data['user_metadata'][attr]['datatype'] == 'series': - result[attr+'_index'] = _data['user_metadata'][attr]['#extra#'] - return result - # Old Metadata API {{{ def print_all_attributes(self): for x in STANDARD_METADATA_FIELDS: prints('%s:'%x, getattr(self, x, 'None')) - for x in self.user_metadata_keys: + for x in self.user_metadata_keys(): meta = self.get_user_metadata(x, make_copy=False) if meta is not None: prints(x, meta) @@ -326,7 +353,7 @@ class Metadata(object): self.cover_data = other.cover_data if getattr(other, 'user_metadata_keys', None): - for x in other.user_metadata_keys: + for x in other.user_metadata_keys(): meta = other.get_user_metadata(x, make_copy=True) if meta is not None: self_tags = self.get(x, []) @@ -389,7 +416,7 @@ class Metadata(object): ''' returns the tuple (field_name, formatted_value) ''' - if key in self.user_metadata_keys: + if key in self.user_metadata_keys(): res = self.get(key, None) cmeta = self.get_user_metadata(key, make_copy=False) if cmeta['datatype'] != 'composite' and (res is None or res == ''): @@ -432,6 +459,9 @@ class Metadata(object): return (None, None, None, None) + def expand_template(self, template): + return format_composite(template, self) + def __unicode__(self): from calibre.ebooks.metadata import authors_to_string ans = [] @@ -466,7 +496,7 @@ class Metadata(object): fmt('Published', isoformat(self.pubdate)) if self.rights is not None: fmt('Rights', unicode(self.rights)) - for key in self.user_metadata_keys: + for key in self.user_metadata_keys(): val = self.get(key, None) if val is not None: (name, val) = self.format_field(key) @@ -491,7 +521,7 @@ class Metadata(object): ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))] if self.rights is not None: ans += [(_('Rights'), unicode(self.rights))] - for key in self.user_metadata_keys: + for key in self.user_metadata_keys(): val = self.get(key, None) if val is not None: (name, val) = self.format_field(key) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 1fb889757f..83cf6278e5 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -270,11 +270,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): def s_r_func(self, match): rfunc = self.s_r_functions[unicode(self.replace_func.currentText())] rtext = unicode(self.replace_with.text()) - mi_data = self.mi.get_all_non_none_attributes() + mi_data = self.mi.all_non_none_fields() def fm_func(m): try: - if m.group(3) not in self.mi.all_keys: return m.group(0) + if m.group(3) not in self.mi.all_field_keys(): return m.group(0) else: return '%s{%s}'%(m.group(1), m.group(3)) except: import traceback diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index be1bf9bc2d..6941869e44 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -319,7 +319,7 @@ class BooksModel(QAbstractTableModel): # {{{ _('Book %s of %s.')%\ (sidx, prepare_string_for_xml(series)) mi = self.db.get_metadata(idx) - for key in mi.user_metadata_keys: + for key in mi.user_metadata_keys(): name, val = mi.format_field(key) if val is not None: data[name] = val diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index f8d50d1cd2..647e31ff51 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -533,6 +533,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ # Save the current field_metadata for applications like calibre2opds # Goes here, because if cf is valid, db is valid. db.prefs['field_metadata'] = db.field_metadata.all_metadata() + if db.gm_count > 0: + print 'get_metadata cache: {0:d} calls, {1:4.2f}% misses'.format( + db.gm_count, (db.gm_missed*100.0)/db.gm_count) for action in self.iactions.values(): if not action.shutting_down(): return diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 770a362a1d..5f7fbdccc9 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -621,7 +621,7 @@ class ResultCache(SearchQueryParser): def multisort(self, fields=[], subsort=False): fields = [(self.sanitize_sort_field_name(x), bool(y)) for x, y in fields] - keys = self.field_metadata.sortable_keys() + 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)] diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 106b498ee8..f5a474edbc 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -325,6 +325,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.has_id = self.data.has_id self.count = self.data.count + # Count times get_metadata is called, and how many times in the cache + self.gm_count = 0 + self.gm_missed = 0 + for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn', 'publisher', 'rating', 'series', 'series_index', 'tags', 'title', 'timestamp', 'uuid', 'pubdate', 'ondevice'): @@ -520,15 +524,47 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): f.close() return ans + ### The field-style interface. These use field keys. + + def get_field(self, idx, key, default=None, index_is_id=False): + mi = self.get_metadata(idx, index_is_id=index_is_id, get_cover=True) + try: + return mi[key] + except: + return default + + def standard_field_keys(self): + return self.field_metadata.standard_field_keys() + + def custom_field_keys(self): + return self.field_metadata.custom_field_keys() + + def all_field_keys(self): + return self.field_metadata.all_field_keys() + + def sortable_field_keys(self): + return self.field_metadata.sortable_field_keys() + + def searchable_fields(self): + return self.field_metadata.searchable_field_keys() + + def search_term_to_field_key(self, term): + return self.field_metadata.search_term_to_key(term) + + def metadata_for_field(self, key): + return self.field_metadata[key] + def get_metadata(self, idx, index_is_id=False, get_cover=False): ''' Convenience method to return metadata as a :class:`Metadata` object. ''' + self.gm_count += 1 mi = self.data.get(idx, self.FIELD_MAP['all_metadata'], row_is_id = index_is_id) if mi is not None: return mi + self.gm_missed += 1 mi = Metadata(None) self.data.set(idx, self.FIELD_MAP['all_metadata'], mi, row_is_id = index_is_id) diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index e4a4f5270d..a8031e5172 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -348,11 +348,24 @@ class FieldMetadata(dict): def keys(self): return self._tb_cats.keys() - def sortable_keys(self): + def sortable_field_keys(self): return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field' and self._tb_cats[k]['datatype'] is not None] + def standard_field_keys(self): + return [k for k in self._tb_cats.keys() + if self._tb_cats[k]['kind']=='field' and + not self._tb_cats[k]['is_custom']] + + def custom_field_keys(self): + return [k for k in self._tb_cats.keys() + if self._tb_cats[k]['kind']=='field' and + self._tb_cats[k]['is_custom']] + + def all_field_keys(self): + return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field'] + def iterkeys(self): for key in self._tb_cats: yield key @@ -474,36 +487,10 @@ class FieldMetadata(dict): key = self.custom_field_prefix+label self._tb_cats[key]['rec_index'] = index # let the exception fly ... - -# DEFAULT_LOCATIONS = frozenset([ -# 'all', -# 'author', # compatibility -# 'authors', -# 'comment', # compatibility -# 'comments', -# 'cover', -# 'date', -# 'format', # compatibility -# 'formats', -# 'isbn', -# 'ondevice', -# 'pubdate', -# 'publisher', -# 'search', -# 'series', -# 'rating', -# 'tag', # compatibility -# 'tags', -# 'title', -# ]) - def get_search_terms(self): s_keys = sorted(self._search_term_map.keys()) for v in self.search_items: s_keys.append(v) -# if set(s_keys) != self.DEFAULT_LOCATIONS: -# print 'search labels and default_locations do not match:' -# print set(s_keys) ^ self.DEFAULT_LOCATIONS return s_keys def _add_search_terms_to_map(self, key, terms): @@ -518,3 +505,8 @@ class FieldMetadata(dict): if term in self._search_term_map: return self._search_term_map[term] return term + + def searchable_field_keys(self): + return [k for k in self._tb_cats.keys() + if self._tb_cats[k]['kind']=='field' and + len(self._tb_cats[k]['search_terms']) > 0] diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index d5300d93e9..fe62dcb7fd 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -125,7 +125,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250, library_order = tweaks['save_template_title_series_sorting'] == 'library_order' tsfmt = title_sort if library_order else lambda x: x format_args = FORMAT_ARGS.copy() - format_args.update(mi.get_all_non_none_attributes()) + format_args.update(mi.all_non_none_fields()) if mi.title: format_args['title'] = tsfmt(mi.title) if mi.authors: diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index c3a662c0fd..041ea78051 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -56,7 +56,7 @@ class ContentServer(object): def sort(self, items, field, order): field = self.db.data.sanitize_sort_field_name(field) - if field not in self.db.field_metadata.sortable_keys(): + if field not in self.db.field_metadata.sortable_field_keys(): raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata) items.sort(key=keyg, reverse=not order)