From b3cbbd3ea8e05e1cd75b12e70d28ca1decc505d4 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 26 Aug 2010 14:32:57 +0100 Subject: [PATCH 01/16] Initial attempt, including full cached metadata, json serialization, and custom fields in save paths. Custom fields in collections probably work, but they haven't been tested. --- src/calibre/devices/apple/driver.py | 7 +- src/calibre/devices/interface.py | 4 +- src/calibre/devices/kobo/books.py | 21 +- src/calibre/devices/usbms/books.py | 43 +-- src/calibre/devices/usbms/driver.py | 21 +- src/calibre/ebooks/metadata/__init__.py | 216 +------------ src/calibre/ebooks/metadata/book/__init__.py | 41 ++- src/calibre/ebooks/metadata/book/base.py | 296 +++++++++++++++--- .../ebooks/metadata/book/json_codec.py | 125 ++++++++ src/calibre/ebooks/metadata/isbndb.py | 6 +- src/calibre/ebooks/metadata/opf2.py | 9 +- .../gui2/dialogs/config/save_template.py | 29 +- src/calibre/gui2/library/models.py | 3 +- src/calibre/library/database2.py | 31 +- src/calibre/library/save_to_disk.py | 19 +- 15 files changed, 514 insertions(+), 357 deletions(-) create mode 100644 src/calibre/ebooks/metadata/book/json_codec.py diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 916c88f203..75517e9df7 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -13,7 +13,8 @@ from calibre.devices.errors import UserFeedback from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.interface import DevicePlugin from calibre.ebooks.BeautifulSoup import BeautifulSoup -from calibre.ebooks.metadata import MetaInformation, authors_to_string +from calibre.ebooks.metadata import authors_to_string +from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.epub import set_metadata from calibre.library.server.utils import strftime from calibre.utils.config import config_dir @@ -2998,14 +2999,14 @@ class BookList(list): ''' return {} -class Book(MetaInformation): +class Book(Metadata): ''' A simple class describing a book in the iTunes Books Library. - See ebooks.metadata.__init__ for all fields ''' def __init__(self,title,author): - MetaInformation.__init__(self, title, authors=[author]) + Metadata.__init__(self, title, authors=[author]) @dynamic_property def title_sorter(self): diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 1384fa03d9..7783173c9c 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -316,7 +316,7 @@ class DevicePlugin(Plugin): being uploaded to the device. :param names: A list of file names that the books should have once uploaded to the device. len(names) == len(files) - :param metadata: If not None, it is a list of :class:`MetaInformation` objects. + :param metadata: If not None, it is a list of :class:`Metadata` objects. The idea is to use the metadata to determine where on the device to put the book. len(metadata) == len(files). Apart from the regular cover (path to cover), there may also be a thumbnail attribute, which should @@ -335,7 +335,7 @@ class DevicePlugin(Plugin): the device. :param locations: Result of a call to L{upload_books} - :param metadata: List of :class:`MetaInformation` objects, same as for + :param metadata: List of :class:`Metadata` objects, same as for :meth:`upload_books`. :param booklists: A tuple containing the result of calls to (:meth:`books(oncard=None)`, diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index a5b2e98d2f..11ab42c29f 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -7,11 +7,11 @@ import os import re import time -from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata.book.base import Metadata from calibre.constants import filesystem_encoding, preferred_encoding from calibre import isbytestring -class Book(MetaInformation): +class Book(Metadata): BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book'] @@ -23,9 +23,9 @@ class Book(MetaInformation): 'uuid', ] - def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, other=None): - - MetaInformation.__init__(self, '') + def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, + thumbnail_name, other=None): + Metadata.__init__(self, '') self.device_collections = [] self._new_book = False @@ -34,7 +34,7 @@ class Book(MetaInformation): self.path = self.path.replace('/', '\\') self.lpath = lpath.replace('\\', '/') else: - self.lpath = lpath + self.lpath = lpath self.title = title if not authors: @@ -52,10 +52,9 @@ class Book(MetaInformation): else: self.datetime = time.gmtime(os.path.getctime(self.path)) except: - self.datetime = time.gmtime() - - if thumbnail_name is not None: - self.thumbnail = ImageWrapper(thumbnail_name) + self.datetime = time.gmtime() + if thumbnail_name is not None: + self.thumbnail = ImageWrapper(thumbnail_name) self.tags = [] if other: self.smart_update(other) @@ -90,7 +89,7 @@ class Book(MetaInformation): in C{other} takes precedence, unless the information in C{other} is NULL. ''' - MetaInformation.smart_update(self, other) + Metadata.smart_update(self, other) for attr in self.BOOK_ATTRS: if hasattr(other, attr): diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 959f26199c..e3c405ee4e 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -6,29 +6,18 @@ __docformat__ = 'restructuredtext en' import os, re, time, sys -from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata.book.base import Metadata from calibre.devices.mime import mime_type_ext from calibre.devices.interface import BookList as _BookList from calibre.constants import filesystem_encoding, preferred_encoding from calibre import isbytestring from calibre.utils.config import prefs -class Book(MetaInformation): - - BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book'] - - JSON_ATTRS = [ - 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', - 'title_sort', 'comments', 'category', 'publisher', 'series', - 'series_index', 'rating', 'isbn', 'language', 'application_id', - 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', - 'uuid', - ] - +class Book(Metadata): def __init__(self, prefix, lpath, size=None, other=None): from calibre.ebooks.metadata.meta import path_to_ext - MetaInformation.__init__(self, '') + Metadata.__init__(self, '') self._new_book = False self.device_collections = [] @@ -72,32 +61,6 @@ class Book(MetaInformation): def thumbnail(self): return None - def smart_update(self, other, replace_metadata=False): - ''' - Merge the information in C{other} into self. In case of conflicts, the information - in C{other} takes precedence, unless the information in C{other} is NULL. - ''' - - MetaInformation.smart_update(self, other, replace_metadata) - - for attr in self.BOOK_ATTRS: - if hasattr(other, attr): - val = getattr(other, attr, None) - setattr(self, attr, val) - - def to_json(self): - json = {} - for attr in self.JSON_ATTRS: - val = getattr(self, attr) - if isbytestring(val): - enc = filesystem_encoding if attr == 'lpath' else preferred_encoding - val = val.decode(enc, 'replace') - elif isinstance(val, (list, tuple)): - val = [x.decode(preferred_encoding, 'replace') if - isbytestring(x) else x for x in val] - json[attr] = val - return json - class BookList(_BookList): def __init__(self, oncard, prefix, settings): diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 0d28f06f49..a0d1d9dbf8 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -13,7 +13,6 @@ for a particular device. import os import re import time -import json from itertools import cycle from calibre import prints, isbytestring @@ -21,6 +20,7 @@ from calibre.constants import filesystem_encoding, DEBUG from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book +from calibre.ebooks.metadata.book.json_codec import JsonCodec BASE_TIME = None def debug_print(*args): @@ -288,6 +288,7 @@ class USBMS(CLI, Device): # at the end just before the return def sync_booklists(self, booklists, end_session=True): debug_print('USBMS: starting sync_booklists') + json_codec = JsonCodec() if not os.path.exists(self.normalize_path(self._main_prefix)): os.makedirs(self.normalize_path(self._main_prefix)) @@ -296,10 +297,8 @@ class USBMS(CLI, Device): if prefix is not None and isinstance(booklists[listid], self.booklist_class): if not os.path.exists(prefix): os.makedirs(self.normalize_path(prefix)) - js = [item.to_json() for item in booklists[listid] if - hasattr(item, 'to_json')] with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: - f.write(json.dumps(js, indent=2, encoding='utf-8')) + json_codec.encode_to_file(f, booklists[listid]) write_prefix(self._main_prefix, 0) write_prefix(self._card_a_prefix, 1) write_prefix(self._card_b_prefix, 2) @@ -345,19 +344,13 @@ class USBMS(CLI, Device): @classmethod def parse_metadata_cache(cls, bl, prefix, name): - # bl = cls.booklist_class() - js = [] + json_codec = JsonCodec() need_sync = False cache_file = cls.normalize_path(os.path.join(prefix, name)) if os.access(cache_file, os.R_OK): try: with open(cache_file, 'rb') as f: - js = json.load(f, encoding='utf-8') - for item in js: - book = cls.book_class(prefix, item.get('lpath', None)) - for key in item.keys(): - setattr(book, key, item[key]) - bl.append(book) + json_codec.decode_from_file(f, bl, cls.book_class, prefix) except: import traceback traceback.print_exc() @@ -392,7 +385,7 @@ class USBMS(CLI, Device): @classmethod def book_from_path(cls, prefix, lpath): - from calibre.ebooks.metadata import MetaInformation + from calibre.ebooks.metadata.book.base import Metadata if cls.settings().read_metadata or cls.MUST_READ_METADATA: mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) @@ -401,7 +394,7 @@ class USBMS(CLI, Device): mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), cls.build_template_regexp()) if mi is None: - mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0], + mi = Metadata(os.path.splitext(os.path.basename(lpath))[0], [_('Unknown')]) size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size book = cls.book_class(prefix, lpath, other=mi, size=size) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index d4a21e2c8c..fb894d3bbd 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -221,214 +221,18 @@ class ResourceCollection(object): -class MetaInformation(object): - '''Convenient encapsulation of book metadata''' - - @staticmethod - def copy(mi): - ans = MetaInformation(mi.title, mi.authors) - for attr in ('author_sort', 'title_sort', 'comments', 'category', - 'publisher', 'series', 'series_index', 'rating', - 'isbn', 'tags', 'cover_data', 'application_id', 'guide', - 'manifest', 'spine', 'toc', 'cover', 'language', - 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', - 'author_sort_map', - 'pubdate', 'rights', 'publication_type', 'uuid'): - if hasattr(mi, attr): - setattr(ans, attr, getattr(mi, attr)) - - def __init__(self, title, authors=(_('Unknown'),)): - ''' +def MetaInformation(title, authors=(_('Unknown'),)): + ''' Convenient encapsulation of book metadata, needed for compatibility @param title: title or ``_('Unknown')`` or a MetaInformation object @param authors: List of strings or [] - ''' - mi = None - if hasattr(title, 'title') and hasattr(title, 'authors'): - mi = title - title = mi.title - authors = mi.authors - self.title = title - self.author = list(authors) if authors else []# Needed for backward compatibility - #: List of strings or [] - self.authors = list(authors) if authors else [] - self.tags = getattr(mi, 'tags', []) - #: mi.cover_data = (ext, data) - self.cover_data = getattr(mi, 'cover_data', (None, None)) - self.author_sort_map = getattr(mi, 'author_sort_map', {}) - - for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher', - 'series', 'series_index', 'rating', 'isbn', 'language', - 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', - 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', - 'rights', 'publication_type', 'uuid', - ): - setattr(self, x, getattr(mi, x, None)) - - def print_all_attributes(self): - for x in ('title','author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher', - 'series', 'series_index', 'tags', 'rating', 'isbn', 'language', - 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', - 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', - 'rights', 'publication_type', 'uuid', 'author_sort_map' - ): - prints(x, getattr(self, x, 'None')) - - def smart_update(self, mi, replace_metadata=False): - ''' - Merge the information in C{mi} into self. In case of conflicts, the - information in C{mi} takes precedence, unless the information in mi is - NULL. If replace_metadata is True, then the information in mi always - takes precedence. - ''' - if mi.title and mi.title != _('Unknown'): - self.title = mi.title - - if mi.authors and mi.authors[0] != _('Unknown'): - self.authors = mi.authors - - for attr in ('author_sort', 'title_sort', 'category', - 'publisher', 'series', 'series_index', 'rating', - 'isbn', 'application_id', 'manifest', 'spine', 'toc', - 'cover', 'guide', 'book_producer', - 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights', - 'publication_type', 'uuid'): - if replace_metadata: - setattr(self, attr, getattr(mi, attr, 1.0 if \ - attr == 'series_index' else None)) - elif hasattr(mi, attr): - val = getattr(mi, attr) - if val is not None: - setattr(self, attr, val) - - if replace_metadata: - self.tags = mi.tags - elif mi.tags: - self.tags += mi.tags - self.tags = list(set(self.tags)) - - if mi.author_sort_map: - self.author_sort_map.update(mi.author_sort_map) - - if getattr(mi, 'cover_data', False): - other_cover = mi.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 = mi.cover_data - - if replace_metadata: - self.comments = getattr(mi, 'comments', '') - else: - my_comments = getattr(self, 'comments', '') - other_comments = getattr(mi, '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(mi, 'language', None) - if other_lang and other_lang.lower() != 'und': - self.language = other_lang - - - def format_series_index(self): - try: - x = float(self.series_index) - except ValueError: - x = 1 - return fmt_sidx(x) - - def authors_from_string(self, raw): - self.authors = string_to_authors(raw) - - def format_authors(self): - return authors_to_string(self.authors) - - def format_tags(self): - return u', '.join([unicode(t) for t in self.tags]) - - def format_rating(self): - return unicode(self.rating) - - def __unicode__(self): - 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.category: - fmt('Category', self.category) - 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)) - if self.lccn: - fmt('LCCN', unicode(self.lccn)) - if self.lcc: - fmt('LCC', unicode(self.lcc)) - if self.ddc: - fmt('DDC', unicode(self.ddc)) - - return u'\n'.join(ans) - - def to_html(self): - 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))] - if self.lccn: - ans += [('LCCN', unicode(self.lccn))] - if self.lcc: - ans += [('LCC', unicode(self.lcc))] - if self.ddc: - ans += [('DDC', unicode(self.ddc))] - 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 i, x in enumerate(ans): - ans[i] = u'
'+_('The template %s is invalid:')%tmpl + \
- '
'+str(err), show=True)
- return False
+ # TODO: 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 save_settings(self, config, name):
val = unicode(self.opt_template.text())
config.set(name, val)
self.opt_template.save_history(self.option_name+'_template_history')
-
-
-
-
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 89008735fe..fdf21ecc23 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -372,7 +372,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 +1052,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/library/database2.py b/src/calibre/library/database2.py
index ef74188bdf..9f21fe0eda 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -509,15 +509,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)
@@ -534,6 +534,10 @@ 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, self.get_custom(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
@@ -1049,6 +1053,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 15020855f7..258ea7ba9e 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -105,6 +105,8 @@ def safe_format(x, format_args):
pass
except AttributeError: # Thrown if user used a non existing attribute
pass
+ except KeyError: # Thrown if user used custom field w/value None
+ pass
return ''
def get_components(template, mi, id, timefmt='%b %Y', length=250,
@@ -113,13 +115,12 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
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.update(mi.all_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 +133,21 @@ 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)
+
+ # These are not necessary any more. The values are set by
+ # 'format_args.update' above, and there is no special formatting
+# if mi.author_sort:
+# format_args['author_sort'] = mi.author_sort
+# if mi.isbn:
+# format_args['isbn'] = mi.isbn
+# if mi.publisher:
+# format_args['publisher'] = mi.publisher
+
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]
From b04faf70c2378c3569a4d1cf010da3d57a707c42 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 27 Aug 2010 08:22:08 +0100
Subject: [PATCH 02/16] Make Kobo driver use new metadata framework
---
src/calibre/devices/kobo/books.py | 80 ++----------------------------
src/calibre/devices/kobo/driver.py | 2 +-
2 files changed, 5 insertions(+), 77 deletions(-)
diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py
index f0cf7c3763..1c3d05ea12 100644
--- a/src/calibre/devices/kobo/books.py
+++ b/src/calibre/devices/kobo/books.py
@@ -4,37 +4,15 @@ __copyright__ = '2010, Timothy Legge '%x
- # CUSTFIELD: What to do about custom fields
+ # TODO: NEWMETA: What to do about custom fields
return u'%s %s %s
'%u'\n'.join(ans)
def __str__(self):
From 11f7bd06a82a0b1d03afffd374344593dfb0c5dc Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 30 Aug 2010 17:47:25 +0100
Subject: [PATCH 07/16] Format custom fields in save_to_disk.
---
src/calibre/library/save_to_disk.py | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 963afd4085..2bc71cde9c 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -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
@@ -131,8 +132,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
- ## TODO: NEWMETA: format custom values. Check all the datatypes.
-
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
if hasattr(mi.timestamp, 'timetuple'):
@@ -140,6 +139,19 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
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]
From 16e5b2f3b0204efefe1c73531c4ce98376342fc7 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 1 Sep 2010 16:44:12 +0100
Subject: [PATCH 08/16] Add code for Sony collections
---
resources/default_tweaks.py | 32 ++++++-
src/calibre/devices/usbms/books.py | 95 ++++++++++++++------
src/calibre/ebooks/metadata/book/__init__.py | 2 +-
src/calibre/ebooks/metadata/book/base.py | 17 ++--
4 files changed, 111 insertions(+), 35 deletions(-)
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index e03b0680be..e68096ecd5 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -90,4 +90,34 @@ save_template_title_series_sorting = 'library_order'
# Examples:
# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
-auto_connect_to_folder = ''
\ No newline at end of file
+auto_connect_to_folder = ''
+
+# Specify renaming rules for sony collections. Collections on Sonys are named
+# depending upon whether the field is standard or custom. A collection derived
+# from a standard field is named for the value in that field. For example, if
+# the standard 'series' column contains the name 'Darkover', then the series
+# will be named 'Darkover'. A collection derived from a custom field will have
+# the name of the field added to the value. For example, if a custom series
+# column named 'My Series' contains the name 'Darkover', then the collection
+# will be named 'Darkover (My Series)'. If two books have fields that generate
+# the same collection name, then both books will be in that collection. This
+# tweak lets you specify for a standard or custom field the value to be put
+# inside the parentheses. You can use it to add a parenthetical description to a
+# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
+# it to force multiple fields to end up in the same collection. For example, you
+# could force the values in 'series', '#my_series_1', and '#my_series_2' to
+# appear in collections named 'some_value (Series)', thereby merging all of the
+# fields into one set of collections. The syntax of this tweak is
+# {'field_lookup_name':'name_to_use', 'lookup_name':'name', ...}
+# Example 1: I want three series columns to be merged into one set of
+# collections. If the column lookup names are 'series', '#series_1' and
+# '#series_2', and if I want nothing in the parenthesis, then the value to use
+# in the tweak value would be:
+# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
+# Example 2: I want the word '(Series)' to appear on collections made from
+# series, and the word '(Tag)' to appear on collections made from tags. Use:
+# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
+# Example 3: I want 'series' and '#myseries' to be merged, and for the
+# collection name to have '(Series)' appended. The renaming rule is:
+# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
+sony_collection_renaming_rules={}
\ No newline at end of file
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 0efa507e09..3e13527bd0 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -9,9 +9,10 @@ import os, re, time, sys
from calibre.ebooks.metadata.book.base import Metadata
from calibre.devices.mime import mime_type_ext
from calibre.devices.interface import BookList as _BookList
-from calibre.constants import filesystem_encoding, preferred_encoding
+from calibre.constants import preferred_encoding
from calibre import isbytestring
-from calibre.utils.config import prefs
+from calibre.utils.config import prefs, tweaks
+from calibre.utils.date import format_date
class Book(Metadata):
def __init__(self, prefix, lpath, size=None, other=None):
@@ -94,11 +95,38 @@ class CollectionsBookList(BookList):
def supports_collections(self):
return True
+ def compute_category_name(self, attr, category, cust_field_meta):
+ renames = tweaks['sony_collection_renaming_rules']
+ attr_name = renames.get(attr, None)
+ if attr_name is None:
+ if attr in cust_field_meta:
+ attr_name = '(%s)'%cust_field_meta[attr]['name']
+ else:
+ attr_name = ''
+ elif attr_name != '':
+ attr_name = '(%s)'%attr_name
+
+ if attr not in cust_field_meta:
+ cat_name = '%s %s'%(category, attr_name)
+ else:
+ fm = cust_field_meta[attr]
+ if fm['datatype'] == 'bool':
+ if category:
+ cat_name = '%s %s'%(_('Yes'), attr_name)
+ else:
+ cat_name = '%s %s'%(_('No'), attr_name)
+ elif fm['datatype'] == 'datetime':
+ cat_name = '%s %s'%(format_date(category,
+ fm['display'].get('date_format','dd MMM yyyy')), attr_name)
+ else:
+ cat_name = '%s %s'%(category, attr_name)
+ return cat_name.strip()
+
def get_collections(self, collection_attributes):
from calibre.devices.usbms.driver import debug_print
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
+ debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
collections = {}
- series_categories = set([])
# This map of sets is used to avoid linear searches when testing for
# book equality
collections_lpaths = {}
@@ -124,42 +152,55 @@ class CollectionsBookList(BookList):
# For existing books, modify the collections only if the user
# specified 'on_connect'
attrs = collection_attributes
+ meta_vals = book.get_all_non_none_attributes()
+ cust_field_meta = book.get_all_user_metadata(make_copy=False)
for attr in attrs:
attr = attr.strip()
- val = getattr(book, attr, None)
+ val = meta_vals.get(attr, None)
if not val: continue
if isbytestring(val):
val = val.decode(preferred_encoding, 'replace')
if isinstance(val, (list, tuple)):
val = list(val)
- elif isinstance(val, unicode):
+ else:
val = [val]
for category in val:
- # TODO: NEWMETA: format the custom fields
- if attr == 'tags' and len(category) > 1 and \
- category[0] == '[' and category[-1] == ']':
+ is_series = False
+ if attr in cust_field_meta: # is a custom field
+ fm = cust_field_meta[attr]
+ if fm['datatype'] == 'text' and len(category) > 1 and \
+ category[0] == '[' and category[-1] == ']':
+ continue
+ if fm['datatype'] == 'series':
+ is_series = True
+ else: # is a standard field
+ if attr == 'tags' and len(category) > 1 and \
+ category[0] == '[' and category[-1] == ']':
+ continue
+ if attr == 'series' or \
+ ('series' in collection_attributes and
+ meta_vals.get('series', None) == category):
+ is_series = True
+ cat_name = self.compute_category_name(attr, category,
+ cust_field_meta)
+ if cat_name not in collections:
+ collections[cat_name] = []
+ collections_lpaths[cat_name] = set()
+ if lpath in collections_lpaths[cat_name]:
continue
- if category not in collections:
- collections[category] = []
- collections_lpaths[category] = set()
- if lpath not in collections_lpaths[category]:
- collections_lpaths[category].add(lpath)
- collections[category].append(book)
- if attr == 'series' or \
- ('series' in collection_attributes and
- getattr(book, 'series', None) == category):
- series_categories.add(category)
+ collections_lpaths[cat_name].add(lpath)
+ if is_series:
+ collections[cat_name].append(
+ (book, meta_vals.get(attr+'_index', sys.maxint)))
+ else:
+ collections[cat_name].append(
+ (book, meta_vals.get('title_sort', 'zzzz')))
# Sort collections
+ result = {}
for category, books in collections.items():
- def tgetter(x):
- return getattr(x, 'title_sort', 'zzzz')
- books.sort(cmp=lambda x,y:cmp(tgetter(x), tgetter(y)))
- if category in series_categories:
- # Ensures books are sub sorted by title
- def getter(x):
- return getattr(x, 'series_index', sys.maxint)
- books.sort(cmp=lambda x,y:cmp(getter(x), getter(y)))
- return collections
+ books.sort(cmp=lambda x,y:cmp(x[1], y[1]))
+ result[category] = [x[0] for x in books]
+ return result
def rebuild_collections(self, booklist, oncard):
'''
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 47eb616394..ca7f4f7074 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -109,7 +109,7 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
CALIBRE_METADATA_FIELDS) - \
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
- 'cover_data', 'tags', 'language'])
+ 'cover_data', 'tags', 'language', 'lpath'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index dcb31c3ecc..3f5507d676 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -117,16 +117,18 @@ class Metadata(object):
res[k] = copy.deepcopy(user_metadata[k])
return res
- def get_user_metadata(self, field):
+ 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, just in case
- the user wants to change values in the dict (json does).
+ 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:
- return copy.deepcopy(_data[field])
+ if make_copy:
+ return copy.deepcopy(_data[field])
+ return _data[field]
return None
@classmethod
@@ -189,7 +191,7 @@ class Metadata(object):
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)
+ meta = self.get_user_metadata(x, make_copy=False)
if meta is not None:
prints(x, meta)
prints('--------------')
@@ -220,6 +222,9 @@ class Metadata(object):
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
self.comments = getattr(other, 'comments', '')
self.language = getattr(other, 'language', None)
+ lpath = getattr(other, 'lpath', None)
+ if lpath is not None:
+ self.lpath = lpath
else:
for attr in COPYABLE_METADATA_FIELDS:
if hasattr(other, attr):
@@ -240,7 +245,7 @@ class Metadata(object):
if getattr(other, 'user_metadata_keys', None):
for x in other.user_metadata_keys:
- meta = other.get_user_metadata(x)
+ meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
self.set_user_metadata(x, meta) # get... did the deepcopy
From 0606afc8ff6edb0dfb6042bbc6cde3f891a068e9 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Sep 2010 13:19:01 +0100
Subject: [PATCH 09/16] 1) make to_html support custom fields 2) clean up the
_extra code 3) add a format_custom_field method to avoid duplicating code 4)
pass custom metadata to book_details
---
src/calibre/ebooks/metadata/book/base.py | 43 ++++++++++++++++++------
src/calibre/gui2/book_details.py | 2 ++
src/calibre/gui2/library/models.py | 6 +++-
src/calibre/library/database2.py | 4 +--
4 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 3f5507d676..caaaccb3d0 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -12,7 +12,8 @@ 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
+from calibre.utils.date import isoformat, format_date
+
NULL_VALUES = {
@@ -94,6 +95,13 @@ class Metadata(object):
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)
@@ -131,14 +139,6 @@ class Metadata(object):
return _data[field]
return None
- @classmethod
- def get_user_metadata_value(user_mi):
- return user_mi['#value#']
-
- @classmethod
- def get_user_metadata_extra(user_mi):
- return user_mi['#extra#']
-
def set_all_user_metadata(self, metadata):
'''
store custom field metadata into the object. Field is the key name
@@ -284,6 +284,25 @@ class Metadata(object):
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(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):
from calibre.ebooks.metadata import authors_to_string
ans = []
@@ -339,9 +358,13 @@ class Metadata(object):
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' '%x
- # TODO: NEWMETA: What to do about custom fields
return u'%s %s %s
'%u'\n'.join(ans)
def __str__(self):
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index f08dd09429..4e11e0c84f 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 fdf21ecc23..2711756856 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):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 6457e12905..bb6d72bcff 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -1052,8 +1052,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id,
- val=Metadata.get_user_metadata_value(user_mi[key]),
- extra=Metadata.get_user_metadata_extra(user_mi[key]),
+ val=mi.get(key),
+ extra=mi.get_extra(key),
label=user_mi[key]['label'])
self.notify('metadata', [id])
From 7ff7da0fbb2485301142a6ba2e6c04923a6d4a59 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 3 Sep 2010 18:43:17 +0100
Subject: [PATCH 10/16] Remove relative import
---
src/calibre/ebooks/metadata/book/json_codec.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 7a80e16854..96178c4a63 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -9,7 +9,7 @@ import json
import traceback
from PIL import Image
-from . import SERIALIZABLE_FIELDS
+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
From 0ba513e2879b5e476d4787b333fb82b8dbe2e914 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 11:01:55 +0100
Subject: [PATCH 11/16] Add a comment about not using the Metadata class, and
why
---
src/calibre/library/server/mobile.py | 4 ++++
src/calibre/library/server/xml.py | 7 ++++---
2 files changed, 8 insertions(+), 3 deletions(-)
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/xml.py b/src/calibre/library/server/xml.py
index ed8479980e..5bf2783f96 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')
@@ -138,6 +142,3 @@ class XMLServer(object):
return etree.tostring(ans, encoding='utf-8', pretty_print=True,
xml_declaration=True)
-
-
-
From 38c6199c7b54b243d039aba25cad86125326a582 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 11:06:54 +0100
Subject: [PATCH 12/16] Change some comments from 'class:MetaInformation' to
'class:Metadata'
---
src/calibre/customize/__init__.py | 4 ++--
src/calibre/devices/apple/driver.py | 2 +-
src/calibre/ebooks/metadata/epub.py | 2 +-
src/calibre/ebooks/metadata/fetch.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py
index 27e319de14..8ddc791b2f 100644
--- a/src/calibre/customize/__init__.py
+++ b/src/calibre/customize/__init__.py
@@ -218,7 +218,7 @@ class MetadataReaderPlugin(Plugin): # {{{
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
- :return: A :class:`calibre.ebooks.metadata.MetaInformation` object
+ :return: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
return None
# }}}
@@ -248,7 +248,7 @@ class MetadataWriterPlugin(Plugin): # {{{
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
- :param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
+ :param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
pass
diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py
index 75517e9df7..94aea1e79d 100644
--- a/src/calibre/devices/apple/driver.py
+++ b/src/calibre/devices/apple/driver.py
@@ -872,7 +872,7 @@ class ITUNES(DriverBase):
once uploaded to the device. len(names) == len(files)
:return: A list of 3-element tuples. The list is meant to be passed
to L{add_books_to_metadata}.
- :metadata: If not None, it is a list of :class:`MetaInformation` objects.
+ :metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to
put the book. len(metadata) == len(files). Apart from the regular
cover (path to cover), there may also be a thumbnail attribute, which should
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).
'''
From 06b266840c8785aa93aa265c0c20308c1c7286f7 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 12:35:35 +0100
Subject: [PATCH 13/16] Make gui version of content server able to show both
abbreviated and full lists of tags.
---
resources/content_server/gui.js | 34 ++++++++++++++++++++++++
src/calibre/ebooks/metadata/book/base.py | 7 ++---
src/calibre/library/server/utils.py | 9 ++++---
src/calibre/library/server/xml.py | 6 +++--
4 files changed, 47 insertions(+), 9 deletions(-)
diff --git a/resources/content_server/gui.js b/resources/content_server/gui.js
index 8368866a5e..abbd409dc8 100644
--- a/resources/content_server/gui.js
+++ b/resources/content_server/gui.js
@@ -59,14 +59,44 @@ function render_book(book) {
title = title.slice(0, title.length-2);
title += ' ({0} MB) '.format(size);
}
+ title += ''
+ if (tags) {
+ t = tags.split(':&:', 2);
+ m = parseInt(t[0]);
+ t = t[1].split(',', m);
+ if (t.length == m) t[m] = '...'
+ title += 'Tags=[{0}] '.format(t.join(','));
+ }
+ custcols = book.attr("custcols").split(',')
+ for ( i = 0; i < custcols.length; i++) {
+ if (custcols[i].length > 0) {
+ vals = book.attr(custcols[i]).split(':#:', 2);
+ if (vals[0].indexOf('#T#') == 0) { //startswith
+ vals[0] = vals[0].substr(3, vals[0].length)
+ t = vals[1].split(':&:', 2);
+ m = parseInt(t[0]);
+ t = t[1].split(',', m);
+ if (t.length == m) t[m] = '...';
+ vals[1] = t.join(',');
+ }
+ title += '{0}=[{1}] '.format(vals[0], vals[1]);
+ }
+ }
+ title += ''
+ title += ' '
title += ' '.format(id);
title += '