diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index dab7b1364d..d0ded25954 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -716,11 +716,13 @@ class DB(object): tables['size'] = SizeTable('size', self.field_metadata['size'].copy()) - self.FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'timestamp':3, - 'size':4, 'rating':5, 'tags':6, 'comments':7, 'series':8, - 'publisher':9, 'series_index':10, 'sort':11, 'author_sort':12, - 'formats':13, 'path':14, 'pubdate':15, 'uuid':16, 'cover':17, - 'au_map':18, 'last_modified':19, 'identifiers':20} + self.FIELD_MAP = { + 'id':0, 'title':1, 'authors':2, 'timestamp':3, 'size':4, + 'rating':5, 'tags':6, 'comments':7, 'series':8, 'publisher':9, + 'series_index':10, 'sort':11, 'author_sort':12, 'formats':13, + 'path':14, 'pubdate':15, 'uuid':16, 'cover':17, 'au_map':18, + 'last_modified':19, 'identifiers':20, 'languages':21, + } for k,v in self.FIELD_MAP.iteritems(): self.field_metadata.set_field_record_index(k, v, prefer_custom=False) @@ -766,6 +768,8 @@ class DB(object): self.field_metadata.set_field_record_index('ondevice', base, prefer_custom=False) self.FIELD_MAP['marked'] = base = base+1 self.field_metadata.set_field_record_index('marked', base, prefer_custom=False) + self.FIELD_MAP['series_sort'] = base = base+1 + self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False) # }}} diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 643af853b3..d1b76cd8bd 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' import os +from functools import partial from calibre.db.backend import DB from calibre.db.cache import Cache @@ -14,6 +15,8 @@ from calibre.db.view import View class LibraryDatabase(object): + ''' Emulate the old LibraryDatabase2 interface ''' + PATH_LIMIT = DB.PATH_LIMIT WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT @@ -30,12 +33,22 @@ class LibraryDatabase(object): backend = self.backend = DB(library_path, default_prefs=default_prefs, read_only=read_only, restore_all_prefs=restore_all_prefs, progress_callback=progress_callback) - cache = Cache(backend) + cache = self.new_api = Cache(backend) cache.init() self.data = View(cache) self.get_property = self.data.get_property - self.all_ids = self.data.cache.all_book_ids + + for prop in ( + 'author_sort', 'authors', 'comment', 'comments', + 'publisher', 'rating', 'series', 'series_index', 'tags', + 'title', 'timestamp', 'uuid', 'pubdate', 'ondevice', + 'metadata_last_modified', 'languages', + ): + fm = {'comment':'comments', 'metadata_last_modified': + 'last_modified', 'title_sort':'sort'}.get(prop, prop) + setattr(self, prop, partial(self.get_property, + loc=self.FIELD_MAP[fm])) def close(self): self.backend.close() @@ -43,7 +56,7 @@ class LibraryDatabase(object): def break_cycles(self): self.data.cache.backend = None self.data.cache = None - self.data = self.backend = self.field_metadata = self.prefs = self.listeners = self.refresh_ondevice = None + self.data = self.backend = self.new_api = self.field_metadata = self.prefs = self.listeners = self.refresh_ondevice = None # Library wide properties {{{ @property @@ -72,6 +85,10 @@ class LibraryDatabase(object): @property def FIELD_MAP(self): return self.backend.FIELD_MAP + + def all_ids(self): + for book_id in self.data.cache.all_book_ids(): + yield book_id # }}} diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 5bb6730bea..6d5734d6b5 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -14,16 +14,21 @@ class LegacyTest(BaseTest): def test_library_wide_properties(self): # {{{ 'Test library wide properties' + def get_props(db): + props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', + 'custom_column_label_map', 'custom_column_num_map') + fprops = ('last_modified', ) + ans = {x:getattr(db, x) for x in props} + ans.update({x:getattr(db, x)() for x in fprops}) + ans['all_ids'] = frozenset(db.all_ids()) + return ans + old = self.init_old() - props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', - 'custom_column_label_map', 'custom_column_num_map') - oldvals = {x:getattr(old, x) for x in props} - oldvals['last_modified'] = old.last_modified() + oldvals = get_props(old) old.close() - old = None + del old db = self.init_legacy() - newvals = {x:getattr(db, x) for x in props} - newvals['last_modified'] = db.last_modified() + newvals = get_props(db) self.assertEqual(oldvals, newvals) db.close() # }}} @@ -38,6 +43,14 @@ class LegacyTest(BaseTest): label = type('')(label) ans[label] = tuple(db.get_property(i, index_is_id=True, loc=loc) for i in db.all_ids()) + if label in ('id', 'title', '#tags'): + with self.assertRaises(IndexError): + db.get_property(9999, loc=loc) + with self.assertRaises(IndexError): + db.get_property(9999, index_is_id=True, loc=loc) + if label in {'tags', 'formats'}: + # Order is random in the old db for these + ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label]) return ans old = self.init_old() diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index c4d0e382a8..4ffa1dd074 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -11,6 +11,9 @@ import weakref from functools import partial from itertools import izip, imap +from calibre.ebooks.metadata import title_sort +from calibre.utils.config_base import tweaks + 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 @@ -40,6 +43,18 @@ class TableRow(list): else: return view._field_getters[obj](self.book_id) +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 x.iteritems()) + class View(object): ''' A table view of the database, with rows and columns. Also supports @@ -53,21 +68,44 @@ class View(object): self.search_restriction_name = self.base_restriction_name = '' self._field_getters = {} for col, idx in cache.backend.FIELD_MAP.iteritems(): + 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, int): label = self.cache.backend.custom_column_num_map[col]['label'] label = (self.cache.backend.field_metadata.custom_field_prefix + label) - self._field_getters[idx] = partial(self.get, label) - else: + if label.endswith('_index'): try: - self._field_getters[idx] = { - 'id': self._get_id, - 'au_map': self.get_author_data, - 'ondevice': self.get_ondevice, - 'marked': self.get_marked, - }[col] - except KeyError: - self._field_getters[idx] = partial(self.get, col) + 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(self.cache.all_book_ids()) self._map_filtered = tuple(self._map) @@ -81,6 +119,8 @@ class View(object): return self.cache.field_metadata def _get_id(self, idx, index_is_id=True): + if index_is_id and idx not in self.cache.all_book_ids(): + raise IndexError('No book with id %s present'%idx) return idx if index_is_id else self.index_to_id(idx) def __getitem__(self, row): @@ -112,9 +152,21 @@ class View(object): def index_to_id(self, idx): return self._map_filtered[idx] - def get(self, field, idx, index_is_id=True, default_value=None): + 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) - return self.cache.field_for(field, id_) + if index_is_id and id_ not in self.cache.all_book_ids(): + 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.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) @@ -124,26 +176,15 @@ class View(object): 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=()): - ''' - Return author data for all authors of the book identified by idx as a - tuple of dictionaries. The dictionaries should never be empty, unless - there is a bug somewhere. The list could be empty if idx point to an - non existent book, or book with no authors (though again a book with no - authors should never happen). - - Each dictionary has the keys: name, sort, link. Link can be an empty - string. - - default_value is ignored, this method always returns a tuple - ''' + 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.read_lock: ids = self.cache._field_ids_for('authors', id_) ans = [] for id_ in ids: - ans.append(self.cache._author_data(id_)) - return tuple(ans) + data = self.cache._author_data(id_) + ans.append(':::'.join((data['name'], data['sort'], data['link']))) + return ':#:'.join(ans) if ans else default_value def multisort(self, fields=[], subsort=False, only_ids=None): fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields]