mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-11-21 05:53:03 -05:00
564 lines
23 KiB
Python
564 lines
23 KiB
Python
'''
|
|
Created on 25 May 2010
|
|
|
|
@author: charles
|
|
'''
|
|
import copy, traceback
|
|
|
|
from calibre.utils.ordered_dict import OrderedDict
|
|
from calibre.utils.config import tweaks
|
|
|
|
class TagsIcons(dict):
|
|
'''
|
|
If the client wants icons to be in the tag structure, this class must be
|
|
instantiated and filled in with real icons. If this class is instantiated
|
|
and passed to get_categories, All items must be given a value not None
|
|
'''
|
|
|
|
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
|
|
'news', 'tags', 'custom:', 'user:', 'search',]
|
|
def __init__(self, icon_dict):
|
|
for a in self.category_icons:
|
|
if a not in icon_dict:
|
|
raise ValueError('Missing category icon [%s]'%a)
|
|
self[a] = icon_dict[a]
|
|
|
|
category_icon_map = {
|
|
'authors' : 'user_profile.png',
|
|
'series' : 'series.png',
|
|
'formats' : 'book.png',
|
|
'publisher' : 'publisher.png',
|
|
'rating' : 'rating.png',
|
|
'news' : 'news.png',
|
|
'tags' : 'tags.png',
|
|
'custom:' : 'column.png',
|
|
'user:' : 'tb_folder.png',
|
|
'search' : 'search.png'
|
|
}
|
|
|
|
|
|
class FieldMetadata(dict):
|
|
'''
|
|
key: the key to the dictionary is:
|
|
- for standard fields, the metadata field name.
|
|
- for custom fields, the metadata field name prefixed by '#'
|
|
This is done to create two 'namespaces' so the names don't clash
|
|
|
|
label: the actual column label. No prefixing.
|
|
|
|
datatype: the type of information in the field. Valid values are listed in
|
|
VALID_DATA_TYPES below.
|
|
is_multiple: valid for the text datatype. If None, the field is to be
|
|
treated as a single term. If not None, it contains a string, and the field
|
|
is assumed to contain a list of terms separated by that string
|
|
|
|
kind == field: is a db field.
|
|
kind == category: standard tag category that isn't a field. see news.
|
|
kind == user: user-defined tag category.
|
|
kind == search: saved-searches category.
|
|
|
|
is_category: is a tag browser category. If true, then:
|
|
table: name of the db table used to construct item list
|
|
column: name of the column in the normalized table to join on
|
|
link_column: name of the column in the connection table to join on. This
|
|
key should not be present if there is no link table
|
|
category_sort: the field in the normalized table to sort on. This
|
|
key must be present if is_category is True
|
|
If these are None, then the category constructor must know how
|
|
to build the item list (e.g., formats, news).
|
|
The order below is the order that the categories will
|
|
appear in the tags pane.
|
|
|
|
name: the text that is to be used when displaying the field. Column headings
|
|
in the GUI, etc.
|
|
|
|
search_terms: the terms that can be used to identify the field when
|
|
searching. They can be thought of as aliases for metadata keys, but are only
|
|
valid when passed to search().
|
|
|
|
is_custom: the field has been added by the user.
|
|
|
|
rec_index: the index of the field in the db metadata record.
|
|
|
|
'''
|
|
|
|
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
|
|
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
|
|
|
# Builtin metadata {{{
|
|
|
|
_field_metadata_prototype = [
|
|
('authors', {'table':'authors',
|
|
'column':'name',
|
|
'link_column':'author',
|
|
'category_sort':'sort',
|
|
'datatype':'text',
|
|
'is_multiple':',',
|
|
'kind':'field',
|
|
'name':_('Authors'),
|
|
'search_terms':['authors', 'author'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('series', {'table':'series',
|
|
'column':'name',
|
|
'link_column':'series',
|
|
'category_sort':'(title_sort(name))',
|
|
'datatype':'series',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Series'),
|
|
'search_terms':['series'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('formats', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':',',
|
|
'kind':'field',
|
|
'name':_('Formats'),
|
|
'search_terms':['formats', 'format'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('publisher', {'table':'publishers',
|
|
'column':'name',
|
|
'link_column':'publisher',
|
|
'category_sort':'name',
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Publishers'),
|
|
'search_terms':['publisher'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('rating', {'table':'ratings',
|
|
'column':'rating',
|
|
'link_column':'rating',
|
|
'category_sort':'rating',
|
|
'datatype':'rating',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Ratings'),
|
|
'search_terms':['rating'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('news', {'table':'news',
|
|
'column':'name',
|
|
'category_sort':'name',
|
|
'datatype':None,
|
|
'is_multiple':None,
|
|
'kind':'category',
|
|
'name':_('News'),
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('tags', {'table':'tags',
|
|
'column':'name',
|
|
'link_column': 'tag',
|
|
'category_sort':'name',
|
|
'datatype':'text',
|
|
'is_multiple':',',
|
|
'kind':'field',
|
|
'name':_('Tags'),
|
|
'search_terms':['tags', 'tag'],
|
|
'is_custom':False,
|
|
'is_category':True}),
|
|
('author_sort',{'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['author_sort'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('au_map', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':',',
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('comments', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Comments'),
|
|
'search_terms':['comments', 'comment'],
|
|
'is_custom':False, 'is_category':False}),
|
|
('cover', {'table':None,
|
|
'column':None,
|
|
'datatype':'int',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['cover'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('flags', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('id', {'table':None,
|
|
'column':None,
|
|
'datatype':'int',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('isbn', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['isbn'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('lccn', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('ondevice', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('On Device'),
|
|
'search_terms':['ondevice'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('path', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('pubdate', {'table':None,
|
|
'column':None,
|
|
'datatype':'datetime',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Published'),
|
|
'search_terms':['pubdate'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('series_index',{'table':None,
|
|
'column':None,
|
|
'datatype':'float',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['series_index'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('sort', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Title Sort'),
|
|
'search_terms':['title_sort'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('size', {'table':None,
|
|
'column':None,
|
|
'datatype':'float',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Size (MB)'),
|
|
'search_terms':['size'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('timestamp', {'table':None,
|
|
'column':None,
|
|
'datatype':'datetime',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Date'),
|
|
'search_terms':['date'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('title', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':_('Title'),
|
|
'search_terms':['title'],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
('uuid', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':None,
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False}),
|
|
]
|
|
# }}}
|
|
|
|
# search labels that are not db columns
|
|
search_items = [ 'all',
|
|
'search',
|
|
]
|
|
|
|
def __init__(self):
|
|
self._field_metadata = copy.deepcopy(self._field_metadata_prototype)
|
|
self._tb_cats = OrderedDict()
|
|
self._search_term_map = {}
|
|
self.custom_label_to_key_map = {}
|
|
for k,v in self._field_metadata:
|
|
if v['kind'] == 'field' and v['datatype'] not in self.VALID_DATA_TYPES:
|
|
raise ValueError('Unknown datatype %s for field %s'%(v['datatype'], k))
|
|
self._tb_cats[k] = v
|
|
self._tb_cats[k]['label'] = k
|
|
self._tb_cats[k]['display'] = {}
|
|
self._tb_cats[k]['is_editable'] = True
|
|
self._add_search_terms_to_map(k, v['search_terms'])
|
|
self._tb_cats['timestamp']['display'] = {
|
|
'date_format': tweaks['gui_timestamp_display_format']}
|
|
self._tb_cats['pubdate']['display'] = {
|
|
'date_format': tweaks['gui_pubdate_display_format']}
|
|
self.custom_field_prefix = '#'
|
|
self.get = self._tb_cats.get
|
|
|
|
def __getitem__(self, key):
|
|
return self._tb_cats[key]
|
|
|
|
def __setitem__(self, key, val):
|
|
raise AttributeError('Assigning to this object is forbidden')
|
|
|
|
def __delitem__(self, key):
|
|
del self._tb_cats[key]
|
|
|
|
def __iter__(self):
|
|
for key in self._tb_cats:
|
|
yield key
|
|
|
|
def __contains__(self, key):
|
|
return self.has_key(key)
|
|
|
|
def has_key(self, key):
|
|
return key in self._tb_cats
|
|
|
|
def keys(self):
|
|
return self._tb_cats.keys()
|
|
|
|
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, include_composites=True):
|
|
res = []
|
|
for k in self._tb_cats.keys():
|
|
fm = self._tb_cats[k]
|
|
if fm['kind']=='field' and fm['is_custom'] and \
|
|
(fm['datatype'] != 'composite' or include_composites):
|
|
res.append(k)
|
|
return res
|
|
|
|
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
|
|
|
|
def itervalues(self):
|
|
return self._tb_cats.itervalues()
|
|
|
|
def values(self):
|
|
return self._tb_cats.values()
|
|
|
|
def iteritems(self):
|
|
for key in self._tb_cats:
|
|
yield (key, self._tb_cats[key])
|
|
|
|
def custom_iteritems(self):
|
|
for key in self._tb_cats:
|
|
fm = self._tb_cats[key]
|
|
if fm['is_custom']:
|
|
yield (key, self._tb_cats[key])
|
|
|
|
def items(self):
|
|
return list(self.iteritems())
|
|
|
|
def is_custom_field(self, key):
|
|
return key.startswith(self.custom_field_prefix)
|
|
|
|
def key_to_label(self, key):
|
|
if 'label' not in self._tb_cats[key]:
|
|
return key
|
|
return self._tb_cats[key]['label']
|
|
|
|
def label_to_key(self, label, prefer_custom=False):
|
|
if prefer_custom:
|
|
if label in self.custom_label_to_key_map:
|
|
return self.custom_label_to_key_map[label]
|
|
if 'label' in self._tb_cats:
|
|
return label
|
|
if not prefer_custom:
|
|
if label in self.custom_label_to_key_map:
|
|
return self.custom_label_to_key_map[label]
|
|
raise ValueError('Unknown key [%s]'%(label))
|
|
|
|
def all_metadata(self):
|
|
l = {}
|
|
for k in self._tb_cats:
|
|
l[k] = self._tb_cats[k]
|
|
return l
|
|
|
|
def custom_field_metadata(self, include_composites=True):
|
|
l = {}
|
|
for k in self.custom_field_keys(include_composites):
|
|
l[k] = self._tb_cats[k]
|
|
return l
|
|
|
|
def add_custom_field(self, label, table, column, datatype, colnum, name,
|
|
display, is_editable, is_multiple, is_category):
|
|
key = self.custom_field_prefix + label
|
|
if key in self._tb_cats:
|
|
raise ValueError('Duplicate custom field [%s]'%(label))
|
|
if datatype not in self.VALID_DATA_TYPES:
|
|
raise ValueError('Unknown datatype %s for field %s'%(datatype, key))
|
|
self._tb_cats[key] = {'table':table, 'column':column,
|
|
'datatype':datatype, 'is_multiple':is_multiple,
|
|
'kind':'field', 'name':name,
|
|
'search_terms':[key], 'label':label,
|
|
'colnum':colnum, 'display':display,
|
|
'is_custom':True, 'is_category':is_category,
|
|
'link_column':'value','category_sort':'value',
|
|
'is_editable': is_editable,}
|
|
self._add_search_terms_to_map(key, [key])
|
|
self.custom_label_to_key_map[label] = key
|
|
if datatype == 'series':
|
|
key += '_index'
|
|
self._tb_cats[key] = {'table':None, 'column':None,
|
|
'datatype':'float', 'is_multiple':None,
|
|
'kind':'field', 'name':'',
|
|
'search_terms':[key], 'label':label+'_index',
|
|
'colnum':None, 'display':{},
|
|
'is_custom':False, 'is_category':False,
|
|
'link_column':None, 'category_sort':None,
|
|
'is_editable': False,}
|
|
self._add_search_terms_to_map(key, [key])
|
|
self.custom_label_to_key_map[label+'_index'] = key
|
|
|
|
def remove_dynamic_categories(self):
|
|
for key in list(self._tb_cats.keys()):
|
|
val = self._tb_cats[key]
|
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
|
for k in self._tb_cats[key]['search_terms']:
|
|
if k in self._search_term_map:
|
|
del self._search_term_map[k]
|
|
del self._tb_cats[key]
|
|
|
|
def remove_user_categories(self):
|
|
for key in list(self._tb_cats.keys()):
|
|
val = self._tb_cats[key]
|
|
if val['is_category'] and val['kind'] == 'user':
|
|
for k in self._tb_cats[key]['search_terms']:
|
|
if k in self._search_term_map:
|
|
del self._search_term_map[k]
|
|
del self._tb_cats[key]
|
|
|
|
def _remove_grouped_search_terms(self):
|
|
to_remove = [v for v in self._search_term_map
|
|
if isinstance(self._search_term_map[v], list)]
|
|
for v in to_remove:
|
|
del self._search_term_map[v]
|
|
|
|
def add_grouped_search_terms(self, gst):
|
|
self._remove_grouped_search_terms()
|
|
for t in gst:
|
|
try:
|
|
self._add_search_terms_to_map(gst[t], [t])
|
|
except ValueError:
|
|
traceback.print_exc()
|
|
|
|
def cc_series_index_column_for(self, key):
|
|
return self._tb_cats[key]['rec_index'] + 1
|
|
|
|
def add_user_category(self, label, name):
|
|
if label in self._tb_cats:
|
|
raise ValueError('Duplicate user field [%s]'%(label))
|
|
st = [label]
|
|
if icu_lower(label) != label:
|
|
st.append(icu_lower(label))
|
|
self._tb_cats[label] = {'table':None, 'column':None,
|
|
'datatype':None, 'is_multiple':None,
|
|
'kind':'user', 'name':name,
|
|
'search_terms':st, 'is_custom':False,
|
|
'is_category':True}
|
|
self._add_search_terms_to_map(label, st)
|
|
|
|
def add_search_category(self, label, name):
|
|
if label in self._tb_cats:
|
|
raise ValueError('Duplicate user field [%s]'%(label))
|
|
self._tb_cats[label] = {'table':None, 'column':None,
|
|
'datatype':None, 'is_multiple':None,
|
|
'kind':'search', 'name':name,
|
|
'search_terms':[], 'is_custom':False,
|
|
'is_category':True}
|
|
|
|
def set_field_record_index(self, label, index, prefer_custom=False):
|
|
if prefer_custom:
|
|
key = self.custom_field_prefix+label
|
|
if key not in self._tb_cats:
|
|
key = label
|
|
else:
|
|
if label in self._tb_cats:
|
|
key = label
|
|
else:
|
|
key = self.custom_field_prefix+label
|
|
self._tb_cats[key]['rec_index'] = index # let the exception fly ...
|
|
|
|
def get_search_terms(self):
|
|
s_keys = sorted(self._search_term_map.keys())
|
|
for v in self.search_items:
|
|
s_keys.append(v)
|
|
return s_keys
|
|
|
|
def _add_search_terms_to_map(self, key, terms):
|
|
if terms is not None:
|
|
for t in terms:
|
|
if t in self._search_term_map:
|
|
raise ValueError('Attempt to add duplicate search term "%s"'%t)
|
|
self._search_term_map[t] = key
|
|
|
|
def search_term_to_field_key(self, term):
|
|
if term in self._search_term_map:
|
|
return self._search_term_map[term]
|
|
return term
|
|
|
|
def searchable_fields(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]
|