mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.9.15+
This commit is contained in:
commit
d2735cc80e
@ -11,6 +11,7 @@ import os, traceback
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
|
|
||||||
|
from calibre.db.categories import get_categories
|
||||||
from calibre.db.locking import create_locks, RecordLock
|
from calibre.db.locking import create_locks, RecordLock
|
||||||
from calibre.db.fields import create_field
|
from calibre.db.fields import create_field
|
||||||
from calibre.db.search import Search
|
from calibre.db.search import Search
|
||||||
@ -293,13 +294,13 @@ class Cache(object):
|
|||||||
Return all the books associated with the item identified by
|
Return all the books associated with the item identified by
|
||||||
``item_id``, where the item belongs to the field ``name``.
|
``item_id``, where the item belongs to the field ``name``.
|
||||||
|
|
||||||
Returned value is a tuple of book ids, or the empty tuple if the item
|
Returned value is a set of book ids, or the empty set if the item
|
||||||
or the field does not exist.
|
or the field does not exist.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
return self.fields[name].books_for(item_id)
|
return self.fields[name].books_for(item_id)
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
return ()
|
return set()
|
||||||
|
|
||||||
@read_api
|
@read_api
|
||||||
def all_book_ids(self, type=frozenset):
|
def all_book_ids(self, type=frozenset):
|
||||||
@ -415,9 +416,7 @@ class Cache(object):
|
|||||||
all_book_ids = frozenset(self._all_book_ids() if ids_to_sort is None
|
all_book_ids = frozenset(self._all_book_ids() if ids_to_sort is None
|
||||||
else ids_to_sort)
|
else ids_to_sort)
|
||||||
get_metadata = partial(self._get_metadata, get_user_categories=False)
|
get_metadata = partial(self._get_metadata, get_user_categories=False)
|
||||||
def get_lang(book_id):
|
lang_map = self.fields['languages'].book_value_map
|
||||||
ans = self._field_for('languages', book_id)
|
|
||||||
return ans[0] if ans else None
|
|
||||||
|
|
||||||
fm = {'title':'sort', 'authors':'author_sort'}
|
fm = {'title':'sort', 'authors':'author_sort'}
|
||||||
|
|
||||||
@ -426,10 +425,10 @@ class Cache(object):
|
|||||||
idx = field + '_index'
|
idx = field + '_index'
|
||||||
is_series = idx in self.fields
|
is_series = idx in self.fields
|
||||||
ans = self.fields[fm.get(field, field)].sort_keys_for_books(
|
ans = self.fields[fm.get(field, field)].sort_keys_for_books(
|
||||||
get_metadata, get_lang, all_book_ids,)
|
get_metadata, lang_map, all_book_ids,)
|
||||||
if is_series:
|
if is_series:
|
||||||
idx_ans = self.fields[idx].sort_keys_for_books(
|
idx_ans = self.fields[idx].sort_keys_for_books(
|
||||||
get_metadata, get_lang, all_book_ids)
|
get_metadata, lang_map, all_book_ids)
|
||||||
ans = {k:(v, idx_ans[k]) for k, v in ans.iteritems()}
|
ans = {k:(v, idx_ans[k]) for k, v in ans.iteritems()}
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
@ -447,6 +446,11 @@ class Cache(object):
|
|||||||
return self._search_api(self, query, restriction,
|
return self._search_api(self, query, restriction,
|
||||||
virtual_fields=virtual_fields)
|
virtual_fields=virtual_fields)
|
||||||
|
|
||||||
|
@read_api
|
||||||
|
def get_categories(self, sort='name', book_ids=None, icon_map=None):
|
||||||
|
return get_categories(self, sort=sort, book_ids=book_ids,
|
||||||
|
icon_map=icon_map)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class SortKey(object):
|
class SortKey(object):
|
||||||
|
118
src/calibre/db/categories.py
Normal file
118
src/calibre/db/categories.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from calibre.library.field_metadata import TagsIcons
|
||||||
|
from calibre.utils.config_base import tweaks
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
|
CATEGORY_SORTS = { 'name', 'popularity', 'rating' }
|
||||||
|
|
||||||
|
class Tag(object):
|
||||||
|
|
||||||
|
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
||||||
|
tooltip=None, icon=None, category=None, id_set=None,
|
||||||
|
is_editable=True, is_searchable=True, use_sort_as_name=False):
|
||||||
|
self.name = self.original_name = name
|
||||||
|
self.id = id
|
||||||
|
self.count = count
|
||||||
|
self.state = state
|
||||||
|
self.is_hierarchical = ''
|
||||||
|
self.is_editable = is_editable
|
||||||
|
self.is_searchable = is_searchable
|
||||||
|
self.id_set = id_set if id_set is not None else set([])
|
||||||
|
self.avg_rating = avg/2.0 if avg is not None else 0
|
||||||
|
self.sort = sort
|
||||||
|
self.use_sort_as_name = use_sort_as_name
|
||||||
|
if self.avg_rating > 0:
|
||||||
|
if tooltip:
|
||||||
|
tooltip = tooltip + ': '
|
||||||
|
tooltip = _('%(tt)sAverage rating is %(rating)3.1f')%dict(
|
||||||
|
tt=tooltip, rating=self.avg_rating)
|
||||||
|
self.tooltip = tooltip
|
||||||
|
self.icon = icon
|
||||||
|
self.category = category
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state,
|
||||||
|
self.category, self.tooltip)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return unicode(self).encode('utf-8')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def find_categories(field_metadata):
|
||||||
|
for category, cat in field_metadata.iteritems():
|
||||||
|
if (cat['is_category'] and cat['kind'] not in {'user', 'search'} and
|
||||||
|
category != 'news'):
|
||||||
|
yield (category, cat['is_multiple'].get('cache_to_list', None), False)
|
||||||
|
elif (cat['datatype'] == 'composite' and
|
||||||
|
cat['display'].get('make_category', False)):
|
||||||
|
yield (category, cat['is_multiple'].get('cache_to_list', None), True)
|
||||||
|
|
||||||
|
def create_tag_class(category, fm, icon_map):
|
||||||
|
cat = fm[category]
|
||||||
|
icon = None
|
||||||
|
tooltip = None if category in {'formats', 'identifiers'} else ('(' + category + ')')
|
||||||
|
label = fm.key_to_label(category)
|
||||||
|
if icon_map:
|
||||||
|
if not fm.is_custom_field(category):
|
||||||
|
if category in icon_map:
|
||||||
|
icon = icon_map[label]
|
||||||
|
else:
|
||||||
|
icon = icon_map['custom:']
|
||||||
|
icon_map[category] = icon
|
||||||
|
is_editable = category not in {'news', 'rating', 'languages', 'formats',
|
||||||
|
'identifiers'}
|
||||||
|
|
||||||
|
if (tweaks['categories_use_field_for_author_name'] == 'author_sort' and
|
||||||
|
(category == 'authors' or
|
||||||
|
(cat['display'].get('is_names', False) and
|
||||||
|
cat['is_custom'] and cat['is_multiple'] and
|
||||||
|
cat['datatype'] == 'text'))):
|
||||||
|
use_sort_as_name = True
|
||||||
|
else:
|
||||||
|
use_sort_as_name = False
|
||||||
|
|
||||||
|
return partial(Tag, use_sort_as_name=use_sort_as_name, icon=icon,
|
||||||
|
tooltip=tooltip, is_editable=is_editable,
|
||||||
|
category=category)
|
||||||
|
|
||||||
|
def get_categories(dbcache, sort='name', book_ids=None, icon_map=None):
|
||||||
|
if icon_map is not None and type(icon_map) != TagsIcons:
|
||||||
|
raise TypeError('icon_map passed to get_categories must be of type TagIcons')
|
||||||
|
if sort not in CATEGORY_SORTS:
|
||||||
|
raise ValueError('sort ' + sort + ' not a valid value')
|
||||||
|
|
||||||
|
fm = dbcache.field_metadata
|
||||||
|
book_rating_map = dbcache.fields['rating'].book_value_map
|
||||||
|
lang_map = dbcache.fields['languages'].book_value_map
|
||||||
|
|
||||||
|
categories = {}
|
||||||
|
book_ids = frozenset(book_ids) if book_ids else book_ids
|
||||||
|
for category, is_multiple, is_composite in find_categories(fm):
|
||||||
|
tag_class = create_tag_class(category, fm, icon_map)
|
||||||
|
cats = dbcache.fields[category].get_categories(
|
||||||
|
tag_class, book_rating_map, lang_map, book_ids)
|
||||||
|
if sort == 'popularity':
|
||||||
|
key=attrgetter('count')
|
||||||
|
elif sort == 'rating':
|
||||||
|
key=attrgetter('avg_rating')
|
||||||
|
else:
|
||||||
|
key=lambda x:sort_key(x.sort or x.name)
|
||||||
|
cats.sort(key=key)
|
||||||
|
categories[category] = cats
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from collections import defaultdict
|
from collections import defaultdict, Counter
|
||||||
|
|
||||||
from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY
|
from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
@ -24,22 +24,26 @@ class Field(object):
|
|||||||
|
|
||||||
def __init__(self, name, table):
|
def __init__(self, name, table):
|
||||||
self.name, self.table = name, table
|
self.name, self.table = name, table
|
||||||
self.has_text_data = self.metadata['datatype'] in ('text', 'comments',
|
|
||||||
'series', 'enumeration')
|
|
||||||
self.table_type = self.table.table_type
|
|
||||||
dt = self.metadata['datatype']
|
dt = self.metadata['datatype']
|
||||||
|
self.has_text_data = dt in {'text', 'comments', 'series', 'enumeration'}
|
||||||
|
self.table_type = self.table.table_type
|
||||||
self._sort_key = (sort_key if dt in ('text', 'series', 'enumeration') else lambda x: x)
|
self._sort_key = (sort_key if dt in ('text', 'series', 'enumeration') else lambda x: x)
|
||||||
self._default_sort_key = ''
|
self._default_sort_key = ''
|
||||||
if self.metadata['datatype'] in ('int', 'float', 'rating'):
|
if dt in { 'int', 'float', 'rating' }:
|
||||||
self._default_sort_key = 0
|
self._default_sort_key = 0
|
||||||
elif self.metadata['datatype'] == 'bool':
|
elif dt == 'bool':
|
||||||
self._default_sort_key = None
|
self._default_sort_key = None
|
||||||
elif self.metadata['datatype'] == 'datetime':
|
elif dt == 'datetime':
|
||||||
self._default_sort_key = UNDEFINED_DATE
|
self._default_sort_key = UNDEFINED_DATE
|
||||||
if self.name == 'languages':
|
if self.name == 'languages':
|
||||||
self._sort_key = lambda x:sort_key(calibre_langcode_to_name(x))
|
self._sort_key = lambda x:sort_key(calibre_langcode_to_name(x))
|
||||||
self.is_multiple = (bool(self.metadata['is_multiple']) or self.name ==
|
self.is_multiple = (bool(self.metadata['is_multiple']) or self.name ==
|
||||||
'formats')
|
'formats')
|
||||||
|
self.category_formatter = type(u'')
|
||||||
|
if dt == 'rating':
|
||||||
|
self.category_formatter = lambda x:'\u2605'*int(x/2)
|
||||||
|
elif name == 'languages':
|
||||||
|
self.category_formatter = calibre_langcode_to_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
@ -63,7 +67,7 @@ class Field(object):
|
|||||||
def books_for(self, item_id):
|
def books_for(self, item_id):
|
||||||
'''
|
'''
|
||||||
Return the ids of all books associated with the item identified by
|
Return the ids of all books associated with the item identified by
|
||||||
item_id as a tuple. An empty tuple is returned if no books are found.
|
item_id as a set. An empty set is returned if no books are found.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -77,7 +81,7 @@ class Field(object):
|
|||||||
'''
|
'''
|
||||||
return iter(())
|
return iter(())
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
'''
|
'''
|
||||||
Return a mapping of book_id -> sort_key. The sort key is suitable for
|
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
|
use in sorting the list of all books by this field, via the python cmp
|
||||||
@ -94,6 +98,27 @@ class Field(object):
|
|||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
|
||||||
|
ans = []
|
||||||
|
if not self.is_many:
|
||||||
|
return ans
|
||||||
|
|
||||||
|
special_sort = hasattr(self, 'category_sort_value')
|
||||||
|
for item_id, item_book_ids in self.table.col_book_map.iteritems():
|
||||||
|
if book_ids is not None:
|
||||||
|
item_book_ids = item_book_ids.intersection(book_ids)
|
||||||
|
if item_book_ids:
|
||||||
|
ratings = tuple(r for r in (book_rating_map.get(book_id, 0) for
|
||||||
|
book_id in item_book_ids) if r > 0)
|
||||||
|
avg = sum(ratings)/len(ratings) if ratings else 0
|
||||||
|
name = self.category_formatter(self.table.id_map[item_id])
|
||||||
|
sval = (self.category_sort_value(item_id, item_book_ids, lang_map)
|
||||||
|
if special_sort else name)
|
||||||
|
c = tag_class(name, id=item_id, sort=sval, avg=avg,
|
||||||
|
id_set=item_book_ids, count=len(item_book_ids))
|
||||||
|
ans.append(c)
|
||||||
|
return ans
|
||||||
|
|
||||||
class OneToOneField(Field):
|
class OneToOneField(Field):
|
||||||
|
|
||||||
def for_book(self, book_id, default_value=None):
|
def for_book(self, book_id, default_value=None):
|
||||||
@ -103,12 +128,12 @@ class OneToOneField(Field):
|
|||||||
return (book_id,)
|
return (book_id,)
|
||||||
|
|
||||||
def books_for(self, item_id):
|
def books_for(self, item_id):
|
||||||
return (item_id,)
|
return {item_id}
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self.table.book_col_map.iterkeys()
|
return self.table.book_col_map.iterkeys()
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
return {id_ : self._sort_key(self.table.book_col_map.get(id_,
|
return {id_ : self._sort_key(self.table.book_col_map.get(id_,
|
||||||
self._default_sort_key)) for id_ in all_book_ids}
|
self._default_sort_key)) for id_ in all_book_ids}
|
||||||
|
|
||||||
@ -150,7 +175,7 @@ class CompositeField(OneToOneField):
|
|||||||
ans = mi.get('#'+self.metadata['label'])
|
ans = mi.get('#'+self.metadata['label'])
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
return {id_ : sort_key(self.get_value_with_cache(id_, get_metadata)) for id_ in
|
return {id_ : sort_key(self.get_value_with_cache(id_, get_metadata)) for id_ in
|
||||||
all_book_ids}
|
all_book_ids}
|
||||||
|
|
||||||
@ -193,7 +218,7 @@ class OnDeviceField(OneToOneField):
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(())
|
return iter(())
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
return {id_ : self.for_book(id_) for id_ in
|
return {id_ : self.for_book(id_) for id_ in
|
||||||
all_book_ids}
|
all_book_ids}
|
||||||
|
|
||||||
@ -223,12 +248,12 @@ class ManyToOneField(Field):
|
|||||||
return (id_,)
|
return (id_,)
|
||||||
|
|
||||||
def books_for(self, item_id):
|
def books_for(self, item_id):
|
||||||
return self.table.col_book_map.get(item_id, ())
|
return self.table.col_book_map.get(item_id, set())
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self.table.id_map.iterkeys()
|
return self.table.id_map.iterkeys()
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
ans = {id_ : self.table.book_col_map.get(id_, None)
|
ans = {id_ : self.table.book_col_map.get(id_, None)
|
||||||
for id_ in all_book_ids}
|
for id_ in all_book_ids}
|
||||||
sk_map = {cid : (self._default_sort_key if cid is None else
|
sk_map = {cid : (self._default_sort_key if cid is None else
|
||||||
@ -238,11 +263,17 @@ class ManyToOneField(Field):
|
|||||||
|
|
||||||
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
|
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
|
||||||
cbm = self.table.col_book_map
|
cbm = self.table.col_book_map
|
||||||
|
empty = set()
|
||||||
for item_id, val in self.table.id_map.iteritems():
|
for item_id, val in self.table.id_map.iteritems():
|
||||||
book_ids = set(cbm.get(item_id, ())).intersection(candidates)
|
book_ids = cbm.get(item_id, empty).intersection(candidates)
|
||||||
if book_ids:
|
if book_ids:
|
||||||
yield val, book_ids
|
yield val, book_ids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def book_value_map(self):
|
||||||
|
return {book_id:self.table.id_map[item_id] for book_id, item_id in
|
||||||
|
self.table.book_col_map.iteritems()}
|
||||||
|
|
||||||
class ManyToManyField(Field):
|
class ManyToManyField(Field):
|
||||||
|
|
||||||
is_many = True
|
is_many = True
|
||||||
@ -263,12 +294,12 @@ class ManyToManyField(Field):
|
|||||||
return self.table.book_col_map.get(book_id, ())
|
return self.table.book_col_map.get(book_id, ())
|
||||||
|
|
||||||
def books_for(self, item_id):
|
def books_for(self, item_id):
|
||||||
return self.table.col_book_map.get(item_id, ())
|
return self.table.col_book_map.get(item_id, set())
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self.table.id_map.iterkeys()
|
return self.table.id_map.iterkeys()
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
ans = {id_ : self.table.book_col_map.get(id_, ())
|
ans = {id_ : self.table.book_col_map.get(id_, ())
|
||||||
for id_ in all_book_ids}
|
for id_ in all_book_ids}
|
||||||
all_cids = set()
|
all_cids = set()
|
||||||
@ -282,8 +313,9 @@ class ManyToManyField(Field):
|
|||||||
|
|
||||||
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
|
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
|
||||||
cbm = self.table.col_book_map
|
cbm = self.table.col_book_map
|
||||||
|
empty = set()
|
||||||
for item_id, val in self.table.id_map.iteritems():
|
for item_id, val in self.table.id_map.iteritems():
|
||||||
book_ids = set(cbm.get(item_id, ())).intersection(candidates)
|
book_ids = cbm.get(item_id, empty).intersection(candidates)
|
||||||
if book_ids:
|
if book_ids:
|
||||||
yield val, book_ids
|
yield val, book_ids
|
||||||
|
|
||||||
@ -295,6 +327,11 @@ class ManyToManyField(Field):
|
|||||||
for count, book_ids in val_map.iteritems():
|
for count, book_ids in val_map.iteritems():
|
||||||
yield count, book_ids
|
yield count, book_ids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def book_value_map(self):
|
||||||
|
return {book_id:tuple(self.table.id_map[item_id] for item_id in item_ids)
|
||||||
|
for book_id, item_ids in self.table.book_col_map.iteritems()}
|
||||||
|
|
||||||
class IdentifiersField(ManyToManyField):
|
class IdentifiersField(ManyToManyField):
|
||||||
|
|
||||||
def for_book(self, book_id, default_value=None):
|
def for_book(self, book_id, default_value=None):
|
||||||
@ -303,7 +340,7 @@ class IdentifiersField(ManyToManyField):
|
|||||||
ids = default_value
|
ids = default_value
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
'Sort by identifier keys'
|
'Sort by identifier keys'
|
||||||
ans = {id_ : self.table.book_col_map.get(id_, ())
|
ans = {id_ : self.table.book_col_map.get(id_, ())
|
||||||
for id_ in all_book_ids}
|
for id_ in all_book_ids}
|
||||||
@ -318,6 +355,17 @@ class IdentifiersField(ManyToManyField):
|
|||||||
if val:
|
if val:
|
||||||
yield val, {book_id}
|
yield val, {book_id}
|
||||||
|
|
||||||
|
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
|
||||||
|
ans = []
|
||||||
|
|
||||||
|
for id_key, item_book_ids in self.table.col_book_map.iteritems():
|
||||||
|
if book_ids is not None:
|
||||||
|
item_book_ids = item_book_ids.intersection(book_ids)
|
||||||
|
if item_book_ids:
|
||||||
|
c = tag_class(id_key, id_set=item_book_ids, count=len(item_book_ids))
|
||||||
|
ans.append(c)
|
||||||
|
return ans
|
||||||
|
|
||||||
class AuthorsField(ManyToManyField):
|
class AuthorsField(ManyToManyField):
|
||||||
|
|
||||||
def author_data(self, author_id):
|
def author_data(self, author_id):
|
||||||
@ -327,6 +375,9 @@ class AuthorsField(ManyToManyField):
|
|||||||
'link' : self.table.alink_map[author_id],
|
'link' : self.table.alink_map[author_id],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def category_sort_value(self, item_id, book_ids, lang_map):
|
||||||
|
return self.table.asort_map[item_id]
|
||||||
|
|
||||||
class FormatsField(ManyToManyField):
|
class FormatsField(ManyToManyField):
|
||||||
|
|
||||||
def for_book(self, book_id, default_value=None):
|
def for_book(self, book_id, default_value=None):
|
||||||
@ -346,21 +397,50 @@ class FormatsField(ManyToManyField):
|
|||||||
for val, book_ids in val_map.iteritems():
|
for val, book_ids in val_map.iteritems():
|
||||||
yield val, book_ids
|
yield val, book_ids
|
||||||
|
|
||||||
|
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
|
||||||
|
ans = []
|
||||||
|
|
||||||
|
for fmt, item_book_ids in self.table.col_book_map.iteritems():
|
||||||
|
if book_ids is not None:
|
||||||
|
item_book_ids = item_book_ids.intersection(book_ids)
|
||||||
|
if item_book_ids:
|
||||||
|
c = tag_class(fmt, id_set=item_book_ids, count=len(item_book_ids))
|
||||||
|
ans.append(c)
|
||||||
|
return ans
|
||||||
|
|
||||||
class SeriesField(ManyToOneField):
|
class SeriesField(ManyToOneField):
|
||||||
|
|
||||||
def sort_key_for_series(self, book_id, get_lang, series_sort_order):
|
def sort_key_for_series(self, book_id, lang_map, series_sort_order):
|
||||||
sid = self.table.book_col_map.get(book_id, None)
|
sid = self.table.book_col_map.get(book_id, None)
|
||||||
if sid is None:
|
if sid is None:
|
||||||
return self._default_sort_key
|
return self._default_sort_key
|
||||||
|
lang = lang_map.get(book_id, None) or None
|
||||||
|
if lang:
|
||||||
|
lang = lang[0]
|
||||||
return self._sort_key(title_sort(self.table.id_map[sid],
|
return self._sort_key(title_sort(self.table.id_map[sid],
|
||||||
order=series_sort_order,
|
order=series_sort_order, lang=lang))
|
||||||
lang=get_lang(book_id)))
|
|
||||||
|
|
||||||
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
|
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
|
||||||
sso = tweaks['title_series_sorting']
|
sso = tweaks['title_series_sorting']
|
||||||
return {book_id:self.sort_key_for_series(book_id, get_lang, sso) for book_id
|
return {book_id:self.sort_key_for_series(book_id, lang_map, sso) for book_id
|
||||||
in all_book_ids}
|
in all_book_ids}
|
||||||
|
|
||||||
|
def category_sort_value(self, item_id, book_ids, lang_map):
|
||||||
|
lang = None
|
||||||
|
tss = tweaks['title_series_sorting']
|
||||||
|
if tss != 'strictly_alphabetic':
|
||||||
|
c = Counter()
|
||||||
|
|
||||||
|
for book_id in book_ids:
|
||||||
|
l = lang_map.get(book_id, None)
|
||||||
|
if l:
|
||||||
|
c[l[0]] += 1
|
||||||
|
|
||||||
|
if c:
|
||||||
|
lang = c.most_common(1)[0][0]
|
||||||
|
val = self.table.id_map[item_id]
|
||||||
|
return title_sort(val, order=tss, lang=lang)
|
||||||
|
|
||||||
def create_field(name, table):
|
def create_field(name, table):
|
||||||
cls = {
|
cls = {
|
||||||
ONE_ONE : OneToOneField,
|
ONE_ONE : OneToOneField,
|
||||||
|
@ -132,13 +132,10 @@ class ManyToOneTable(Table):
|
|||||||
'SELECT book, {0} FROM {1}'.format(
|
'SELECT book, {0} FROM {1}'.format(
|
||||||
self.metadata['link_column'], self.link_table)):
|
self.metadata['link_column'], self.link_table)):
|
||||||
if row[1] not in self.col_book_map:
|
if row[1] not in self.col_book_map:
|
||||||
self.col_book_map[row[1]] = []
|
self.col_book_map[row[1]] = set()
|
||||||
self.col_book_map[row[1]].append(row[0])
|
self.col_book_map[row[1]].add(row[0])
|
||||||
self.book_col_map[row[0]] = row[1]
|
self.book_col_map[row[0]] = row[1]
|
||||||
|
|
||||||
for key in tuple(self.col_book_map.iterkeys()):
|
|
||||||
self.col_book_map[key] = tuple(self.col_book_map[key])
|
|
||||||
|
|
||||||
class ManyToManyTable(ManyToOneTable):
|
class ManyToManyTable(ManyToOneTable):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -154,15 +151,12 @@ class ManyToManyTable(ManyToOneTable):
|
|||||||
for row in db.conn.execute(
|
for row in db.conn.execute(
|
||||||
self.selectq.format(self.metadata['link_column'], self.link_table)):
|
self.selectq.format(self.metadata['link_column'], self.link_table)):
|
||||||
if row[1] not in self.col_book_map:
|
if row[1] not in self.col_book_map:
|
||||||
self.col_book_map[row[1]] = []
|
self.col_book_map[row[1]] = set()
|
||||||
self.col_book_map[row[1]].append(row[0])
|
self.col_book_map[row[1]].add(row[0])
|
||||||
if row[0] not in self.book_col_map:
|
if row[0] not in self.book_col_map:
|
||||||
self.book_col_map[row[0]] = []
|
self.book_col_map[row[0]] = []
|
||||||
self.book_col_map[row[0]].append(row[1])
|
self.book_col_map[row[0]].append(row[1])
|
||||||
|
|
||||||
for key in tuple(self.col_book_map.iterkeys()):
|
|
||||||
self.col_book_map[key] = tuple(self.col_book_map[key])
|
|
||||||
|
|
||||||
for key in tuple(self.book_col_map.iterkeys()):
|
for key in tuple(self.book_col_map.iterkeys()):
|
||||||
self.book_col_map[key] = tuple(self.book_col_map[key])
|
self.book_col_map[key] = tuple(self.book_col_map[key])
|
||||||
|
|
||||||
@ -191,8 +185,8 @@ class FormatsTable(ManyToManyTable):
|
|||||||
if row[1] is not None:
|
if row[1] is not None:
|
||||||
fmt = row[1].upper()
|
fmt = row[1].upper()
|
||||||
if fmt not in self.col_book_map:
|
if fmt not in self.col_book_map:
|
||||||
self.col_book_map[fmt] = []
|
self.col_book_map[fmt] = set()
|
||||||
self.col_book_map[fmt].append(row[0])
|
self.col_book_map[fmt].add(row[0])
|
||||||
if row[0] not in self.book_col_map:
|
if row[0] not in self.book_col_map:
|
||||||
self.book_col_map[row[0]] = []
|
self.book_col_map[row[0]] = []
|
||||||
self.book_col_map[row[0]].append(fmt)
|
self.book_col_map[row[0]].append(fmt)
|
||||||
@ -200,9 +194,6 @@ class FormatsTable(ManyToManyTable):
|
|||||||
self.fname_map[row[0]] = {}
|
self.fname_map[row[0]] = {}
|
||||||
self.fname_map[row[0]][fmt] = row[2]
|
self.fname_map[row[0]][fmt] = row[2]
|
||||||
|
|
||||||
for key in tuple(self.col_book_map.iterkeys()):
|
|
||||||
self.col_book_map[key] = tuple(self.col_book_map[key])
|
|
||||||
|
|
||||||
for key in tuple(self.book_col_map.iterkeys()):
|
for key in tuple(self.book_col_map.iterkeys()):
|
||||||
self.book_col_map[key] = tuple(sorted(self.book_col_map[key]))
|
self.book_col_map[key] = tuple(sorted(self.book_col_map[key]))
|
||||||
|
|
||||||
@ -215,15 +206,12 @@ class IdentifiersTable(ManyToManyTable):
|
|||||||
for row in db.conn.execute('SELECT book, type, val FROM identifiers'):
|
for row in db.conn.execute('SELECT book, type, val FROM identifiers'):
|
||||||
if row[1] is not None and row[2] is not None:
|
if row[1] is not None and row[2] is not None:
|
||||||
if row[1] not in self.col_book_map:
|
if row[1] not in self.col_book_map:
|
||||||
self.col_book_map[row[1]] = []
|
self.col_book_map[row[1]] = set()
|
||||||
self.col_book_map[row[1]].append(row[0])
|
self.col_book_map[row[1]].add(row[0])
|
||||||
if row[0] not in self.book_col_map:
|
if row[0] not in self.book_col_map:
|
||||||
self.book_col_map[row[0]] = {}
|
self.book_col_map[row[0]] = {}
|
||||||
self.book_col_map[row[0]][row[1]] = row[2]
|
self.book_col_map[row[0]][row[1]] = row[2]
|
||||||
|
|
||||||
for key in tuple(self.col_book_map.iterkeys()):
|
|
||||||
self.col_book_map[key] = tuple(self.col_book_map[key])
|
|
||||||
|
|
||||||
class LanguagesTable(ManyToManyTable):
|
class LanguagesTable(ManyToManyTable):
|
||||||
|
|
||||||
def read_id_maps(self, db):
|
def read_id_maps(self, db):
|
||||||
|
@ -42,6 +42,7 @@ class BaseTest(unittest.TestCase):
|
|||||||
if attr == 'format_metadata': continue # TODO: Not implemented yet
|
if attr == 'format_metadata': continue # TODO: Not implemented yet
|
||||||
attr1, attr2 = getattr(mi1, attr), getattr(mi2, attr)
|
attr1, attr2 = getattr(mi1, attr), getattr(mi2, attr)
|
||||||
if attr == 'formats':
|
if attr == 'formats':
|
||||||
|
continue # TODO: Not implemented yet
|
||||||
attr1, attr2 = map(lambda x:tuple(x) if x else (), (attr1, attr2))
|
attr1, attr2 = map(lambda x:tuple(x) if x else (), (attr1, attr2))
|
||||||
self.assertEqual(attr1, attr2,
|
self.assertEqual(attr1, attr2,
|
||||||
'%s not the same: %r != %r'%(attr, attr1, attr2))
|
'%s not the same: %r != %r'%(attr, attr1, attr2))
|
||||||
|
@ -241,6 +241,18 @@ class ReadingTest(BaseTest):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
def test_get_categories(self): # {{{
|
||||||
|
'Check that get_categories() returns the same data for both backends'
|
||||||
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
|
old = LibraryDatabase2(self.library_path)
|
||||||
|
old_categories = old.get_categories()
|
||||||
|
cache = self.init_cache(self.library_path)
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(old_categories)
|
||||||
|
pprint.pprint(cache.get_categories())
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
def tests():
|
def tests():
|
||||||
return unittest.TestLoader().loadTestsFromTestCase(ReadingTest)
|
return unittest.TestLoader().loadTestsFromTestCase(ReadingTest)
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ class ANDROID(USBMS):
|
|||||||
|
|
||||||
# LG
|
# LG
|
||||||
0x1004 : {
|
0x1004 : {
|
||||||
0x61c5 : [0x100, 0x226, 0x227, 0x9999],
|
0x61c5 : [0x100, 0x226, 0x227, 0x229, 0x9999],
|
||||||
0x61cc : [0x226, 0x227, 0x9999, 0x100],
|
0x61cc : [0x226, 0x227, 0x9999, 0x100],
|
||||||
0x61ce : [0x226, 0x227, 0x9999, 0x100],
|
0x61ce : [0x226, 0x227, 0x9999, 0x100],
|
||||||
0x618e : [0x226, 0x227, 0x9999, 0x100],
|
0x618e : [0x226, 0x227, 0x9999, 0x100],
|
||||||
@ -235,7 +235,7 @@ class ANDROID(USBMS):
|
|||||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
||||||
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
||||||
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
|
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
|
||||||
'ICS']
|
'ICS', 'E400']
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||||
@ -246,7 +246,7 @@ class ANDROID(USBMS):
|
|||||||
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
|
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
|
||||||
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
|
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
|
||||||
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
|
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
|
||||||
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F']
|
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD']
|
||||||
|
|
||||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||||
|
|
||||||
|
@ -311,6 +311,14 @@ class ITUNES(DriverBase):
|
|||||||
update_msg = None
|
update_msg = None
|
||||||
update_needed = False
|
update_needed = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_dir(self):
|
||||||
|
return os.path.join(cache_dir(), 'itunes')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def archive_path(self):
|
||||||
|
return os.path.join(self.cache_dir, "thumbs.zip")
|
||||||
|
|
||||||
# Public methods
|
# Public methods
|
||||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||||
'''
|
'''
|
||||||
|
@ -44,47 +44,13 @@ from calibre.utils.recycle_bin import delete_file, delete_tree
|
|||||||
from calibre.utils.formatter_functions import load_user_template_functions
|
from calibre.utils.formatter_functions import load_user_template_functions
|
||||||
from calibre.db.errors import NoSuchFormat
|
from calibre.db.errors import NoSuchFormat
|
||||||
from calibre.db.lazy import FormatMetadata, FormatsList
|
from calibre.db.lazy import FormatMetadata, FormatsList
|
||||||
|
from calibre.db.categories import Tag
|
||||||
from calibre.utils.localization import (canonicalize_lang,
|
from calibre.utils.localization import (canonicalize_lang,
|
||||||
calibre_langcode_to_name)
|
calibre_langcode_to_name)
|
||||||
|
|
||||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
SPOOL_SIZE = 30*1024*1024
|
SPOOL_SIZE = 30*1024*1024
|
||||||
|
|
||||||
class Tag(object):
|
|
||||||
|
|
||||||
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
|
||||||
tooltip=None, icon=None, category=None, id_set=None,
|
|
||||||
is_editable = True, is_searchable=True, use_sort_as_name=False):
|
|
||||||
self.name = self.original_name = name
|
|
||||||
self.id = id
|
|
||||||
self.count = count
|
|
||||||
self.state = state
|
|
||||||
self.is_hierarchical = ''
|
|
||||||
self.is_editable = is_editable
|
|
||||||
self.is_searchable = is_searchable
|
|
||||||
self.id_set = id_set if id_set is not None else set([])
|
|
||||||
self.avg_rating = avg/2.0 if avg is not None else 0
|
|
||||||
self.sort = sort
|
|
||||||
self.use_sort_as_name = use_sort_as_name
|
|
||||||
if self.avg_rating > 0:
|
|
||||||
if tooltip:
|
|
||||||
tooltip = tooltip + ': '
|
|
||||||
tooltip = _('%(tt)sAverage rating is %(rating)3.1f')%dict(
|
|
||||||
tt=tooltip, rating=self.avg_rating)
|
|
||||||
self.tooltip = tooltip
|
|
||||||
self.icon = icon
|
|
||||||
self.category = category
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return u'%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state,
|
|
||||||
self.category, self.tooltip)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return unicode(self).encode('utf-8')
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||||
'''
|
'''
|
||||||
An ebook metadata database that stores references to ebook files on disk.
|
An ebook metadata database that stores references to ebook files on disk.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user