'
__docformat__ = 'restructuredtext en'
import copy
+import traceback
+
+from calibre import prints
+from calibre.ebooks.metadata.book import COPYABLE_METADATA_FIELDS
+from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
+from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
+from calibre.utils.date import isoformat, format_date
+
-from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
NULL_VALUES = {
'user_metadata': {},
@@ -19,103 +26,356 @@ NULL_VALUES = {
'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
+ 'language' : 'und'
}
class Metadata(object):
'''
- This class must expose a superset of the API of MetaInformation in terms
- of attribute access and methods. Only the __init__ method is different.
- MetaInformation will simply become a function that creates and fills in
- the attributes of this class.
+ A class representing all the metadata for a book.
Please keep the method based API of this class to a minimum. Every method
becomes a reserved field name.
'''
- def __init__(self):
+ def __init__(self, title, authors=(_('Unknown'),), other=None):
+ '''
+ @param title: title or ``_('Unknown')``
+ @param authors: List of strings or []
+ @param other: None or a metadata object
+ '''
object.__setattr__(self, '_data', copy.deepcopy(NULL_VALUES))
+ if other is not None:
+ self.smart_update(other)
+ else:
+ if title:
+ self.title = title
+ if authors:
+ #: List of strings or []
+ self.author = list(authors) if authors else []# Needed for backward compatibility
+ self.authors = list(authors) if authors else []
def __getattribute__(self, field):
_data = object.__getattribute__(self, '_data')
- if field in RESERVED_METADATA_FIELDS:
+ if field in TOP_LEVEL_CLASSIFIERS:
+ return _data.get('classifiers').get(field, None)
+ if field in STANDARD_METADATA_FIELDS:
return _data.get(field, None)
try:
return object.__getattribute__(self, field)
except AttributeError:
pass
if field in _data['user_metadata'].iterkeys():
- # TODO: getting user metadata values
- pass
+ return _data['user_metadata'][field]['#value#']
raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))
-
- def __setattr__(self, field, val):
+ def __setattr__(self, field, val, extra=None):
_data = object.__getattribute__(self, '_data')
- if field in RESERVED_METADATA_FIELDS:
- if field != 'user_metadata':
- if not val:
- val = NULL_VALUES[field]
- _data[field] = val
- else:
- raise AttributeError('You cannot set user_metadata directly.')
+ if field in TOP_LEVEL_CLASSIFIERS:
+ _data['classifiers'].update({field: val})
+ elif field in STANDARD_METADATA_FIELDS:
+ if val is None:
+ val = NULL_VALUES.get(field, None)
+ _data[field] = val
elif field in _data['user_metadata'].iterkeys():
- # TODO: Setting custom column values
- pass
+ _data['user_metadata'][field]['#value#'] = val
+ _data['user_metadata'][field]['#extra#'] = extra
else:
# You are allowed to stick arbitrary attributes onto this object as
- # long as they dont conflict with global or user metadata names
+ # long as they don't conflict with global or user metadata names
# Don't abuse this privilege
self.__dict__[field] = val
+ def get(self, field, default=None):
+ if default is not None:
+ try:
+ return self.__getattribute__(field)
+ except AttributeError:
+ return default
+ return self.__getattribute__(field)
+
+ def get_extra(self, field):
+ _data = object.__getattribute__(self, '_data')
+ if field in _data['user_metadata'].iterkeys():
+ return _data['user_metadata'][field]['#extra#']
+ raise AttributeError(
+ 'Metadata object has no attribute named: '+ repr(field))
+
+ def set(self, field, val, extra=None):
+ self.__setattr__(field, val, extra)
+
@property
- def user_metadata_names(self):
+ def user_metadata_keys(self):
'The set of user metadata names this object knows about'
_data = object.__getattribute__(self, '_data')
return frozenset(_data['user_metadata'].iterkeys())
- # Old MetaInformation API {{{
- def copy(self):
- pass
+ def get_all_user_metadata(self, make_copy):
+ '''
+ return a dict containing all the custom field metadata associated with
+ the book.
+ '''
+ _data = object.__getattribute__(self, '_data')
+ user_metadata = _data['user_metadata']
+ if not make_copy:
+ return user_metadata
+ res = {}
+ for k in user_metadata:
+ res[k] = copy.deepcopy(user_metadata[k])
+ return res
+ def get_user_metadata(self, field, make_copy):
+ '''
+ return field metadata from the object if it is there. Otherwise return
+ None. field is the key name, not the label. Return a copy if requested,
+ just in case the user wants to change values in the dict.
+ '''
+ _data = object.__getattribute__(self, '_data')
+ _data = _data['user_metadata']
+ if field in _data:
+ if make_copy:
+ return copy.deepcopy(_data[field])
+ return _data[field]
+ return None
+
+ def set_all_user_metadata(self, metadata):
+ '''
+ store custom field metadata into the object. Field is the key name
+ not the label
+ '''
+ if metadata is None:
+ traceback.print_stack()
+ else:
+ for key in metadata:
+ self.set_user_metadata(key, metadata[key])
+
+ def set_user_metadata(self, field, metadata):
+ '''
+ store custom field metadata for one column into the object. Field is
+ the key name not the label
+ '''
+ if field is not None:
+ if metadata is None:
+ traceback.print_stack()
+ metadata = copy.deepcopy(metadata)
+ if '#value#' not in metadata:
+ if metadata['datatype'] == 'text' and metadata['is_multiple']:
+ metadata['#value#'] = []
+ else:
+ metadata['#value#'] = None
+ _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):
- pass
+ for x in STANDARD_METADATA_FIELDS:
+ prints('%s:'%x, getattr(self, x, 'None'))
+ for x in self.user_metadata_keys:
+ meta = self.get_user_metadata(x, make_copy=False)
+ if meta is not None:
+ prints(x, meta)
+ prints('--------------')
def smart_update(self, other, replace_metadata=False):
- pass
+ '''
+ Merge the information in C{other} into self. In case of conflicts, the information
+ in C{other} takes precedence, unless the information in other is NULL.
+ '''
+ def copy_not_none(dest, src, attr):
+ v = getattr(src, attr, None)
+ if v is not None:
+ setattr(dest, attr, copy.deepcopy(v))
- def format_series_index(self):
- pass
+ if other.title and other.title != _('Unknown'):
+ self.title = other.title
+ if hasattr(other, 'title_sort'):
+ self.title_sort = other.title_sort
+
+ if other.authors and other.authors[0] != _('Unknown'):
+ self.authors = other.authors
+ if hasattr(other, 'author_sort_map'):
+ self.author_sort_map = other.author_sort_map
+ if hasattr(other, 'author_sort'):
+ self.author_sort = other.author_sort
+
+ if replace_metadata:
+ for attr in COPYABLE_METADATA_FIELDS:
+ setattr(self, attr, getattr(other, attr, 1.0 if \
+ attr == 'series_index' else None))
+ self.tags = other.tags
+ self.cover_data = getattr(other, 'cover_data', '')
+ self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
+ copy_not_none(self, other, 'lpath')
+ copy_not_none(self, other, 'size')
+ copy_not_none(self, other, 'comments')
+ # language is handled below
+ else:
+ for attr in COPYABLE_METADATA_FIELDS:
+ if hasattr(other, attr):
+ copy_not_none(self, other, attr)
+ val = getattr(other, attr)
+ if val is not None:
+ setattr(self, attr, copy.deepcopy(val))
+ if other.tags:
+ self.tags += list(set(self.tags + other.tags))
+ if getattr(other, 'cover_data', False):
+ other_cover = other.cover_data[-1]
+ self_cover = self.cover_data[-1] if self.cover_data else ''
+ if not self_cover: self_cover = ''
+ if not other_cover: other_cover = ''
+ if len(other_cover) > len(self_cover):
+ self.cover_data = other.cover_data
+ if getattr(other, 'user_metadata_keys', None):
+ for x in other.user_metadata_keys:
+ meta = other.get_user_metadata(x, make_copy=True)
+ if meta is not None:
+ self.set_user_metadata(x, meta) # get... did the deepcopy
+ my_comments = getattr(self, 'comments', '')
+ other_comments = getattr(other, 'comments', '')
+ if not my_comments:
+ my_comments = ''
+ if not other_comments:
+ other_comments = ''
+ if len(other_comments.strip()) > len(my_comments.strip()):
+ self.comments = other_comments
+ other_lang = getattr(other, 'language', None)
+ if other_lang and other_lang.lower() != 'und':
+ self.language = other_lang
+
+ def format_series_index(self, val=None):
+ from calibre.ebooks.metadata import fmt_sidx
+ v = self.series_index if val is None else val
+ try:
+ x = float(v)
+ except ValueError:
+ x = 1
+ return fmt_sidx(x)
def authors_from_string(self, raw):
- pass
+ from calibre.ebooks.metadata import string_to_authors
+ self.authors = string_to_authors(raw)
def format_authors(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ return authors_to_string(self.authors)
def format_tags(self):
- pass
+ return u', '.join([unicode(t) for t in self.tags])
def format_rating(self):
return unicode(self.rating)
+ def format_custom_field(self, key):
+ '''
+ returns the tuple (field_name, formatted_value)
+ '''
+ cmeta = self.get_user_metadata(key, make_copy=False)
+ name = unicode(cmeta['name'])
+ res = self.get(key, None)
+ if res is not None:
+ datatype = cmeta['datatype']
+ if datatype == 'text' and cmeta['is_multiple']:
+ res = u', '.join(res)
+ elif datatype == 'series':
+ res = res + ' [%s]'%self.format_series_index(val=self.get_extra(key))
+ elif datatype == 'datetime':
+ res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
+ elif datatype == 'bool':
+ res = _('Yes') if res else _('No')
+ return (name, unicode(res))
+
def __unicode__(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ ans = []
+ def fmt(x, y):
+ ans.append(u'%-20s: %s'%(unicode(x), unicode(y)))
+
+ fmt('Title', self.title)
+ if self.title_sort:
+ fmt('Title sort', self.title_sort)
+ if self.authors:
+ fmt('Author(s)', authors_to_string(self.authors) + \
+ ((' [' + self.author_sort + ']') if self.author_sort else ''))
+ if self.publisher:
+ fmt('Publisher', self.publisher)
+ if getattr(self, 'book_producer', False):
+ fmt('Book Producer', self.book_producer)
+ if self.comments:
+ fmt('Comments', self.comments)
+ if self.isbn:
+ fmt('ISBN', self.isbn)
+ if self.tags:
+ fmt('Tags', u', '.join([unicode(t) for t in self.tags]))
+ if self.series:
+ fmt('Series', self.series + ' #%s'%self.format_series_index())
+ if self.language:
+ fmt('Language', self.language)
+ if self.rating is not None:
+ fmt('Rating', self.rating)
+ if self.timestamp is not None:
+ fmt('Timestamp', isoformat(self.timestamp))
+ if self.pubdate is not None:
+ fmt('Published', isoformat(self.pubdate))
+ if self.rights is not None:
+ fmt('Rights', unicode(self.rights))
+ for key in self.user_metadata_keys:
+ val = self.get(key, None)
+ if val is not None:
+ (name, val) = self.format_custom_field(key)
+ fmt(name, unicode(val))
+ return u'\n'.join(ans)
def to_html(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ ans = [(_('Title'), unicode(self.title))]
+ ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
+ ans += [(_('Publisher'), unicode(self.publisher))]
+ ans += [(_('Producer'), unicode(self.book_producer))]
+ ans += [(_('Comments'), unicode(self.comments))]
+ ans += [('ISBN', unicode(self.isbn))]
+ ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
+ if self.series:
+ ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
+ ans += [(_('Language'), unicode(self.language))]
+ if self.timestamp is not None:
+ ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
+ if self.pubdate is not None:
+ ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
+ if self.rights is not None:
+ ans += [(_('Rights'), unicode(self.rights))]
+ for key in self.user_metadata_keys:
+ val = self.get(key, None)
+ if val is not None:
+ (name, val) = self.format_custom_field(key)
+ ans += [(name, val)]
+ for i, x in enumerate(ans):
+ ans[i] = u'%s | %s |
'%x
+ return u''%u'\n'.join(ans)
def __str__(self):
return self.__unicode__().encode('utf-8')
def __nonzero__(self):
- return True
+ return bool(self.title or self.author or self.comments or self.tags)
# }}}
-
-# We don't need reserved field names for this object any more. Lets just use a
-# protocol like the last char of a user field label should be _ when using this
-# object
-# So mi.tags returns the builtin tags and mi.tags_ returns the user tags
-
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
new file mode 100644
index 0000000000..0e205c52b0
--- /dev/null
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -0,0 +1,124 @@
+'''
+Created on 4 Jun 2010
+
+@author: charles
+'''
+
+from base64 import b64encode, b64decode
+import json
+import traceback
+
+from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
+from calibre.constants import filesystem_encoding, preferred_encoding
+from calibre.library.field_metadata import FieldMetadata
+from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
+
+# Translate datetimes to and from strings. The string form is the datetime in
+# UTC. The returned date is also UTC
+def string_to_datetime(src):
+ if src == "None":
+ return None
+# dt = strptime(src, '%d %m %Y %H:%M:%S', assume_utc=True, as_utc=True)
+# if dt == UNDEFINED_DATE:
+# return None
+ return parse_date(src)
+
+def datetime_to_string(dateval):
+ if dateval is None or dateval == UNDEFINED_DATE:
+ return "None"
+# tt = date_to_utc(dateval).timetuple()
+# res = "%02d %02d %04d %02d:%02d:%02d"%(tt.tm_mday, tt.tm_mon, tt.tm_year,
+# tt.tm_hour, tt.tm_min, tt.tm_sec)
+ return isoformat(dateval)
+
+def encode_thumbnail(thumbnail):
+ '''
+ Encode the image part of a thumbnail, then return the 3 part tuple
+ '''
+ if thumbnail is None:
+ return None
+ return (thumbnail[0], thumbnail[1], b64encode(str(thumbnail[2])))
+
+def decode_thumbnail(tup):
+ '''
+ Decode an encoded thumbnail into its 3 component parts
+ '''
+ if tup is None:
+ return None
+ return (tup[0], tup[1], b64decode(tup[2]))
+
+class JsonCodec(object):
+
+ def __init__(self):
+ self.field_metadata = FieldMetadata()
+
+ def encode_to_file(self, file, booklist):
+ json.dump(self.encode_booklist_metadata(booklist), file, indent=2, encoding='utf-8')
+
+ def encode_booklist_metadata(self, booklist):
+ result = []
+ for book in booklist:
+ result.append(self.encode_book_metadata(book))
+ return result
+
+ def encode_book_metadata(self, book):
+ result = {}
+ for key in SERIALIZABLE_FIELDS:
+ result[key] = self.encode_metadata_attr(book, key)
+ return result
+
+ def encode_metadata_attr(self, book, key):
+ if key == 'user_metadata':
+ meta = book.get_all_user_metadata(make_copy=True)
+ for k in meta:
+ if meta[k]['datatype'] == 'datetime':
+ meta[k]['#value#'] = datetime_to_string(meta[k]['#value#'])
+ return meta
+ if key in self.field_metadata:
+ datatype = self.field_metadata[key]['datatype']
+ else:
+ datatype = None
+ value = book.get(key)
+ if key == 'thumbnail':
+ return encode_thumbnail(value)
+ elif isinstance(value, str): # str includes bytes
+ enc = filesystem_encoding if key == 'lpath' else preferred_encoding
+ return value.decode(enc, 'replace')
+ elif isinstance(value, (list, tuple)):
+ return [x.decode(preferred_encoding, 'replace') if
+ isinstance(x, str) else x for x in value]
+ elif datatype == 'datetime':
+ return datetime_to_string(value)
+ else:
+ return value
+
+ def decode_from_file(self, file, booklist, book_class, prefix):
+ js = []
+ try:
+ js = json.load(file, encoding='utf-8')
+ for item in js:
+ book = book_class(prefix, item.get('lpath', None))
+ for key in item.keys():
+ meta = self.decode_metadata(key, item[key])
+ if key == 'user_metadata':
+ book.set_all_user_metadata(meta)
+ else:
+ setattr(book, key, meta)
+ booklist.append(book)
+ except:
+ print 'exception during JSON decoding'
+ traceback.print_exc()
+ booklist = []
+
+ def decode_metadata(self, key, value):
+ if key == 'user_metadata':
+ for k in value:
+ if value[k]['datatype'] == 'datetime':
+ value[k]['#value#'] = string_to_datetime(value[k]['#value#'])
+ return value
+ elif key in self.field_metadata:
+ if self.field_metadata[key]['datatype'] == 'datetime':
+ return string_to_datetime(value)
+ if key == 'thumbnail':
+ return decode_thumbnail(value)
+ return value
diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py
index 041a1ee603..ac6b5feebe 100644
--- a/src/calibre/ebooks/metadata/epub.py
+++ b/src/calibre/ebooks/metadata/epub.py
@@ -164,7 +164,7 @@ def get_cover(opf, opf_path, stream, reader=None):
return render_html_svg_workaround(cpage, default_log)
def get_metadata(stream, extract_cover=True):
- """ Return metadata as a :class:`MetaInformation` object """
+ """ Return metadata as a :class:`Metadata` object """
stream.seek(0)
reader = OCFZipReader(stream)
mi = MetaInformation(reader.opf)
diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py
index 96807c06ae..9b8a42e482 100644
--- a/src/calibre/ebooks/metadata/fetch.py
+++ b/src/calibre/ebooks/metadata/fetch.py
@@ -29,7 +29,7 @@ class MetadataSource(Plugin): # {{{
future use.
The fetch method must store the results in `self.results` as a list of
- :class:`MetaInformation` objects. If there is an error, it should be stored
+ :class:`Metadata` objects. If there is an error, it should be stored
in `self.exception` and `self.tb` (for the traceback).
'''
diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py
index 356cc3f1b1..b5fc5830c8 100644
--- a/src/calibre/ebooks/metadata/isbndb.py
+++ b/src/calibre/ebooks/metadata/isbndb.py
@@ -8,7 +8,7 @@ import sys, re
from urllib import quote
from calibre.utils.config import OptionParser
-from calibre.ebooks.metadata import MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre import browser
@@ -42,10 +42,10 @@ def fetch_metadata(url, max=100, timeout=5.):
return books
-class ISBNDBMetadata(MetaInformation):
+class ISBNDBMetadata(Metadata):
def __init__(self, book):
- MetaInformation.__init__(self, None, [])
+ Metadata.__init__(self, None, [])
self.isbn = book.get('isbn13', book.get('isbn'))
self.title = book.find('titlelong').string
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index f93b614ef2..9f17bbd15c 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -16,7 +16,8 @@ from lxml import etree
from calibre.ebooks.chardet import xml_to_unicode
from calibre.constants import __appname__, __version__, filesystem_encoding
from calibre.ebooks.metadata.toc import TOC
-from calibre.ebooks.metadata import MetaInformation, string_to_authors
+from calibre.ebooks.metadata import string_to_authors, MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_date, isoformat
from calibre.utils.localization import get_lang
@@ -926,16 +927,16 @@ class OPF(object):
setattr(self, attr, val)
-class OPFCreator(MetaInformation):
+class OPFCreator(Metadata):
- def __init__(self, base_path, *args, **kwargs):
+ def __init__(self, base_path, other):
'''
Initialize.
@param base_path: An absolute path to the directory in which this OPF file
will eventually be. This is used by the L{create_manifest} method
to convert paths to files into relative paths.
'''
- MetaInformation.__init__(self, *args, **kwargs)
+ Metadata.__init__(self, title='', other=other)
self.base_path = os.path.abspath(base_path)
if self.application_id is None:
self.application_id = str(uuid.uuid4())
@@ -1187,7 +1188,7 @@ def metadata_to_opf(mi, as_string=True):
factory(DC('contributor'), mi.book_producer, __appname__, 'bkp')
if hasattr(mi.pubdate, 'isoformat'):
factory(DC('date'), isoformat(mi.pubdate))
- if mi.category:
+ if hasattr(mi, 'category') and mi.category:
factory(DC('type'), mi.category)
if mi.comments:
factory(DC('description'), mi.comments)
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index 6b8d4b1d3c..cfb582024d 100644
--- a/src/calibre/gui2/book_details.py
+++ b/src/calibre/gui2/book_details.py
@@ -28,6 +28,8 @@ WEIGHTS[_('Tags')] = 4
def render_rows(data):
keys = data.keys()
+ # First sort by name. The WEIGHTS sort will preserve this sub-order
+ keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y]))
rows = []
for key in keys:
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index ae9487f801..5e51f2447c 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -323,7 +323,11 @@ class BooksModel(QAbstractTableModel): # {{{
data[_('Series')] = \
_('Book %s of %s.')%\
(sidx, prepare_string_for_xml(series))
-
+ mi = self.db.get_metadata(idx)
+ for key in mi.user_metadata_keys:
+ name, val = mi.format_custom_field(key)
+ if val is not None:
+ data[name] = val
return data
def set_cache(self, idx):
@@ -372,7 +376,6 @@ class BooksModel(QAbstractTableModel): # {{{
return ans
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
- # Should this add the custom columns? It doesn't at the moment
metadata, _full_metadata = [], []
if not rows_are_ids:
rows = [self.db.id(row.row()) for row in rows]
@@ -1053,7 +1056,7 @@ class DeviceBooksModel(BooksModel): # {{{
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
else:
- img.loadFromData(cdata)
+ img.loadFromData(cdata[2])
if img.isNull():
img = self.default_image
data['cover'] = img
diff --git a/src/calibre/gui2/preferences/save_template.py b/src/calibre/gui2/preferences/save_template.py
index d325ac42ff..26dc02f259 100644
--- a/src/calibre/gui2/preferences/save_template.py
+++ b/src/calibre/gui2/preferences/save_template.py
@@ -8,10 +8,8 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget, pyqtSignal
-from calibre.gui2 import error_dialog
from calibre.gui2.preferences.save_template_ui import Ui_Form
-from calibre.library.save_to_disk import FORMAT_ARG_DESCS, \
- preprocess_template
+from calibre.library.save_to_disk import FORMAT_ARG_DESCS
class SaveTemplate(QWidget, Ui_Form):
@@ -41,18 +39,21 @@ class SaveTemplate(QWidget, Ui_Form):
self.changed_signal.emit()
def validate(self):
- tmpl = preprocess_template(self.opt_template.text())
- fa = {}
- for x in FORMAT_ARG_DESCS.keys():
- fa[x]='random long string'
- try:
- tmpl.format(**fa)
- except Exception, err:
- error_dialog(self, _('Invalid template'),
- ''+_('The template %s is invalid:')%tmpl + \
- '
'+str(err), show=True)
- return False
+ # TODO: NEWMETA: I haven't figured out how to get the custom columns
+ # into here, so for the moment make all templates valid.
return True
+# tmpl = preprocess_template(self.opt_template.text())
+# fa = {}
+# for x in FORMAT_ARG_DESCS.keys():
+# fa[x]='random long string'
+# try:
+# tmpl.format(**fa)
+# except Exception, err:
+# error_dialog(self, _('Invalid template'),
+# '
'+_('The template %s is invalid:')%tmpl + \
+# '
'+str(err), show=True)
+# return False
+# return True
def set_value(self, val):
self.opt_template.set_value(val)
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index cc4ddb1c17..c3878618e6 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -521,15 +521,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''
Convenience method to return metadata as a L{MetaInformation} object.
'''
- aum = self.authors(idx, index_is_id=index_is_id)
- if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
+ aut_list = self.authors_with_sort_strings(idx, index_is_id=index_is_id)
+ aum = []
+ aus = {}
+ for (author, author_sort) in aut_list:
+ aum.append(author)
+ aus[author] = author_sort
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
- if mi.authors:
- mi.author_sort_map = {}
- for name, sort in zip(mi.authors, self.authors_sort_strings(idx,
- index_is_id)):
- mi.author_sort_map[name] = sort
+ mi.author_sort_map = aus
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@@ -546,6 +546,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.isbn = self.isbn(idx, index_is_id=index_is_id)
id = idx if index_is_id else self.id(idx)
mi.application_id = id
+ for key,meta in self.field_metadata.iteritems():
+ if meta['is_custom']:
+ mi.set_user_metadata(key, meta)
+ mi.set(key, val=self.get_custom(idx, label=meta['label'],
+ index_is_id=index_is_id),
+ extra=self.get_custom_extra(idx, label=meta['label'],
+ index_is_id=index_is_id))
if get_cover:
mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi
@@ -1084,6 +1091,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False)
self.set_path(id, True)
+
+ user_mi = mi.get_all_user_metadata(make_copy=False)
+ for key in user_mi.iterkeys():
+ if key in self.field_metadata and \
+ user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
+ doit(self.set_custom, id,
+ val=mi.get(key),
+ extra=mi.get_extra(key),
+ label=user_mi[key]['label'])
self.notify('metadata', [id])
# Given a book, return the list of author sort strings for the book's authors
@@ -1099,6 +1115,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
result.append(sort)
return result
+ # Given a book, return the map of author sort strings for the book's authors
+ def authors_with_sort_strings(self, id, index_is_id=False):
+ id = id if index_is_id else self.id(id)
+ aut_strings = self.conn.get('''
+ SELECT authors.name, authors.sort
+ FROM authors, books_authors_link as bl
+ WHERE bl.book=? and authors.id=bl.author
+ ORDER BY bl.id''', (id,))
+ result = []
+ for (author, sort,) in aut_strings:
+ result.append((author.replace('|', ','), sort))
+ return result
+
# Given a book, return the author_sort string for authors of the book
def author_sort_from_book(self, id, index_is_id=False):
auts = self.authors_sort_strings(id, index_is_id)
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index f5c4063789..3fa40c68b2 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import os, traceback, cStringIO, re
+import os, traceback, cStringIO, re, string
from calibre.utils.config import Config, StringConfig, tweaks
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
@@ -14,6 +14,7 @@ from calibre.utils.filenames import shorten_components_to, supports_long_names,
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import preferred_encoding, filesystem_encoding
+from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata import title_sort
from calibre import strftime
@@ -97,29 +98,33 @@ def preprocess_template(template):
template = template.decode(preferred_encoding, 'replace')
return template
+class SafeFormat(string.Formatter):
+ '''
+ Provides a format function that substitutes '' for any missing value
+ '''
+ def get_value(self, key, args, kwargs):
+ try:
+ return kwargs[key]
+ except:
+ return ''
+safe_formatter = SafeFormat()
+
def safe_format(x, format_args):
- try:
- ans = x.format(**format_args).strip()
- return re.sub(r'\s+', ' ', ans)
- except IndexError: # Thrown if user used [] and index is out of bounds
- pass
- except AttributeError: # Thrown if user used a non existing attribute
- pass
- return ''
+ ans = safe_formatter.vformat(x, [], format_args).strip()
+ return re.sub(r'\s+', ' ', ans)
def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False,
to_lowercase=False):
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
tsfmt = title_sort if library_order else lambda x: x
- format_args = dict(**FORMAT_ARGS)
+ format_args = FORMAT_ARGS.copy()
+ format_args.update(mi.get_all_non_none_attributes())
if mi.title:
format_args['title'] = tsfmt(mi.title)
if mi.authors:
format_args['authors'] = mi.format_authors()
format_args['author'] = format_args['authors']
- if mi.author_sort:
- format_args['author_sort'] = mi.author_sort
if mi.tags:
format_args['tags'] = mi.format_tags()
if format_args['tags'].startswith('/'):
@@ -132,15 +137,25 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
template = re.sub(r'\{series_index[^}]*?\}', '', template)
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
- if mi.isbn:
- format_args['isbn'] = mi.isbn
- if mi.publisher:
- format_args['publisher'] = mi.publisher
if hasattr(mi.timestamp, 'timetuple'):
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
if hasattr(mi.pubdate, 'timetuple'):
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
format_args['id'] = str(id)
+ # Now format the custom fields
+ custom_metadata = mi.get_all_user_metadata(make_copy=False)
+ for key in custom_metadata:
+ if key in format_args:
+ ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
+ if custom_metadata[key]['datatype'] == 'series':
+ format_args[key] = tsfmt(format_args[key])
+ if key+'_index' in format_args:
+ format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
+ elif custom_metadata[key]['datatype'] == 'datetime':
+ format_args[key] = strftime(timefmt, format_args[key].timetuple())
+ elif custom_metadata[key]['datatype'] == 'bool':
+ format_args[key] = _('yes') if format_args[key] else _('no')
+
components = [x.strip() for x in template.split('/') if x.strip()]
components = [safe_format(x, format_args) for x in components]
components = [sanitize_func(x) for x in components if x]
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index 229e0c21c4..6e08581aed 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -199,6 +199,10 @@ class MobileServer(object):
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
+ # This method uses its own book dict, not the Metadata dict. The loop
+ # below could be changed to use db.get_metadata instead of reading
+ # info directly from the record made by the view, but it doesn't seem
+ # worth it at the moment.
books = []
for record in items[(start-1):(start-1)+num]:
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index 23916aa75c..373653c15f 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import time
+import time, sys
import cherrypy
@@ -44,8 +44,8 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except:
return _strftime(fmt, nowf().timetuple())
-def format_tag_string(tags, sep):
- MAX = tweaks['max_content_server_tags_shown']
+def format_tag_string(tags, sep, ignore_max=False):
+ MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
else:
@@ -53,5 +53,6 @@ def format_tag_string(tags, sep):
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
- return u'%s'%(', '.join(tlist)) if tlist else ''
+ return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
+ ', '.join(tlist)) if tlist else ''
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index ed8479980e..8715dda7d0 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -66,6 +66,10 @@ class XMLServer(object):
return x.decode(preferred_encoding, 'replace')
return unicode(x)
+ # This method uses its own book dict, not the Metadata dict. The loop
+ # below could be changed to use db.get_metadata instead of reading
+ # info directly from the record made by the view, but it doesn't seem
+ # worth it at the moment.
for record in items[start:start+num]:
kwargs = {}
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
@@ -85,7 +89,7 @@ class XMLServer(object):
'comments'):
y = record[FM[x]]
if x == 'tags':
- y = format_tag_string(y, ',')
+ y = format_tag_string(y, ',', ignore_max=True)
kwargs[x] = serialize(y) if y else ''
c = kwargs.pop('comments')
@@ -107,7 +111,9 @@ class XMLServer(object):
name = CFM[key]['name']
custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']:
- kwargs[k] = concat(name, format_tag_string(val,'|'))
+ kwargs[k] = concat('#T#'+name,
+ format_tag_string(val,'|',
+ ignore_max=True))
elif datatype == 'series':
kwargs[k] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
@@ -138,6 +144,3 @@ class XMLServer(object):
return etree.tostring(ans, encoding='utf-8', pretty_print=True,
xml_declaration=True)
-
-
-