diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 10fe0bb014..a631f9ea46 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -269,11 +269,11 @@ class Cache(object): return () @read_api - def all_book_ids(self): + def all_book_ids(self, type=frozenset): ''' Frozen set of all known book ids. ''' - return frozenset(self.fields['uuid']) + return type(self.fields['uuid']) @read_api def all_field_ids(self, name): @@ -316,6 +316,10 @@ class Cache(object): self.format_metadata_cache[book_id][fmt] = ans return ans + @read_api + def pref(self, name): + return self.backend.prefs[name] + @api def get_metadata(self, book_id, get_cover=False, get_user_categories=True, cover_as_data=False): @@ -378,17 +382,21 @@ class Cache(object): all_book_ids = frozenset(self._all_book_ids() if ids_to_sort is None else ids_to_sort) get_metadata = partial(self._get_metadata, get_user_categories=False) + def get_lang(book_id): + ans = self._field_for('languages', book_id) + return ans[0] if ans else None fm = {'title':'sort', 'authors':'author_sort'} def sort_key(field): 'Handle series type fields' - ans = self.fields[fm.get(field, field)].sort_keys_for_books(get_metadata, - all_book_ids) idx = field + '_index' - if idx in self.fields: - idx_ans = self.fields[idx].sort_keys_for_books(get_metadata, - all_book_ids) + is_series = idx in self.fields + ans = self.fields[fm.get(field, field)].sort_keys_for_books( + get_metadata, get_lang, all_book_ids,) + if is_series: + idx_ans = self.fields[idx].sort_keys_for_books( + get_metadata, get_lang, all_book_ids) ans = {k:(v, idx_ans[k]) for k, v in ans.iteritems()} return ans diff --git a/src/calibre/db/fields.py b/src/calibre/db/fields.py index e154900031..3808052549 100644 --- a/src/calibre/db/fields.py +++ b/src/calibre/db/fields.py @@ -11,6 +11,8 @@ __docformat__ = 'restructuredtext en' from threading import Lock from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY +from calibre.ebooks.metadata import title_sort +from calibre.utils.config_base import tweaks from calibre.utils.icu import sort_key from calibre.utils.date import UNDEFINED_DATE from calibre.utils.localization import calibre_langcode_to_name @@ -72,7 +74,7 @@ class Field(object): ''' return iter(()) - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): ''' Return a mapping of book_id -> sort_key. The sort key is suitable for use in sorting the list of all books by this field, via the python cmp @@ -96,7 +98,7 @@ class OneToOneField(Field): def __iter__(self): return self.table.book_col_map.iterkeys() - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): return {id_ : self._sort_key(self.table.book_col_map.get(id_, self._default_sort_key)) for id_ in all_book_ids} @@ -133,7 +135,7 @@ class CompositeField(OneToOneField): ans = mi.get('#'+self.metadata['label']) return ans - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): return {id_ : sort_key(self.get_value_with_cache(id_, get_metadata)) for id_ in all_book_ids} @@ -170,7 +172,7 @@ class OnDeviceField(OneToOneField): def __iter__(self): return iter(()) - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): return {id_ : self.for_book(id_) for id_ in all_book_ids} @@ -196,7 +198,7 @@ class ManyToOneField(Field): def __iter__(self): return self.table.id_map.iterkeys() - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): ans = {id_ : self.table.book_col_map.get(id_, None) for id_ in all_book_ids} sk_map = {cid : (self._default_sort_key if cid is None else @@ -227,7 +229,7 @@ class ManyToManyField(Field): def __iter__(self): return self.table.id_map.iterkeys() - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): ans = {id_ : self.table.book_col_map.get(id_, ()) for id_ in all_book_ids} all_cids = set() @@ -248,7 +250,7 @@ class IdentifiersField(ManyToManyField): ids = default_value return ids - def sort_keys_for_books(self, get_metadata, all_book_ids): + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): 'Sort by identifier keys' ans = {id_ : self.table.book_col_map.get(id_, ()) for id_ in all_book_ids} @@ -274,6 +276,21 @@ class FormatsField(ManyToManyField): def format_fname(self, book_id, fmt): return self.table.fname_map[book_id][fmt.upper()] +class SeriesField(ManyToOneField): + + def sort_key_for_series(self, book_id, get_lang, series_sort_order): + sid = self.table.book_col_map.get(book_id, None) + if sid is None: + return self._default_sort_key + return self._sort_key(title_sort(self.table.id_map[sid], + order=series_sort_order, + lang=get_lang(book_id))) + + def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids): + sso = tweaks['title_series_sorting'] + return {book_id:self.sort_key_for_series(book_id, get_lang, sso) for book_id + in all_book_ids} + def create_field(name, table): cls = { ONE_ONE : OneToOneField, @@ -290,5 +307,7 @@ def create_field(name, table): cls = IdentifiersField elif table.metadata['datatype'] == 'composite': cls = CompositeField + elif table.metadata['datatype'] == 'series': + cls = SeriesField return cls(name, table) diff --git a/src/calibre/db/tests/metadata.db b/src/calibre/db/tests/metadata.db index 63a096e2f4..da54c61ad5 100644 Binary files a/src/calibre/db/tests/metadata.db and b/src/calibre/db/tests/metadata.db differ diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index b722d30793..d77d3ac6eb 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -63,7 +63,7 @@ class ReadingTest(BaseTest): 'sort': 'One', 'authors': ('Author One',), 'author_sort': 'One, Author', - 'series' : 'Series One', + 'series' : 'A Series One', 'series_index': 1.0, 'tags':('Tag Two', 'Tag One'), 'formats': (), @@ -92,7 +92,7 @@ class ReadingTest(BaseTest): 'sort': 'Title Two', 'authors': ('Author Two', 'Author One'), 'author_sort': 'Two, Author & One, Author', - 'series' : 'Series One', + 'series' : 'A Series One', 'series_index': 2.0, 'rating': 6.0, 'tags': ('Tag One',), @@ -130,30 +130,31 @@ class ReadingTest(BaseTest): 'Test sorting' cache = self.init_cache(self.library_path) for field, order in { - 'title' : [2, 1, 3], - 'authors': [2, 1, 3], - 'series' : [3, 2, 1], - 'tags' : [3, 1, 2], - 'rating' : [3, 2, 1], - # 'identifiers': [3, 2, 1], There is no stable sort since 1 and - # 2 have the same identifier keys - # TODO: Add an empty book to the db and ensure that empty - # fields sort the same as they do in db2 - 'timestamp': [2, 1, 3], - 'pubdate' : [1, 2, 3], - 'publisher': [3, 2, 1], - 'last_modified': [2, 1, 3], - 'languages': [3, 2, 1], - 'comments': [3, 2, 1], - '#enum' : [3, 2, 1], - '#authors' : [3, 2, 1], - '#date': [3, 1, 2], - '#rating':[3, 2, 1], - '#series':[3, 2, 1], - '#tags':[3, 2, 1], - '#yesno':[3, 1, 2], - '#comments':[3, 2, 1], - }.iteritems(): + 'title' : [2, 1, 3], + 'authors': [2, 1, 3], + 'series' : [3, 1, 2], + 'tags' : [3, 1, 2], + 'rating' : [3, 2, 1], + # 'identifiers': [3, 2, 1], There is no stable sort since 1 and + # 2 have the same identifier keys + # 'last_modified': [3, 2, 1], There is no stable sort as two + # records have the exact same value + 'timestamp': [2, 1, 3], + 'pubdate' : [1, 2, 3], + 'publisher': [3, 2, 1], + 'languages': [3, 2, 1], + 'comments': [3, 2, 1], + '#enum' : [3, 2, 1], + '#authors' : [3, 2, 1], + '#date': [3, 1, 2], + '#rating':[3, 2, 1], + '#series':[3, 2, 1], + '#tags':[3, 2, 1], + '#yesno':[3, 1, 2], + '#comments':[3, 2, 1], + # TODO: Add an empty book to the db and ensure that empty + # fields sort the same as they do in db2 + }.iteritems(): x = list(reversed(order)) self.assertEqual(order, cache.multisort([(field, True)], ids_to_sort=x),