mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Refactor metadata object to support custom columns
This commit is contained in:
commit
1e4baacd51
@ -59,14 +59,44 @@ function render_book(book) {
|
|||||||
title = title.slice(0, title.length-2);
|
title = title.slice(0, title.length-2);
|
||||||
title += ' ({0} MB) '.format(size);
|
title += ' ({0} MB) '.format(size);
|
||||||
}
|
}
|
||||||
|
title += '<span class="tagdata_short" style="display:all">'
|
||||||
|
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 += '</span>'
|
||||||
|
title += '<span class="tagdata_long" style="display:none">'
|
||||||
if (tags) title += 'Tags=[{0}] '.format(tags);
|
if (tags) title += 'Tags=[{0}] '.format(tags);
|
||||||
custcols = book.attr("custcols").split(',')
|
custcols = book.attr("custcols").split(',')
|
||||||
for ( i = 0; i < custcols.length; i++) {
|
for ( i = 0; i < custcols.length; i++) {
|
||||||
if (custcols[i].length > 0) {
|
if (custcols[i].length > 0) {
|
||||||
vals = book.attr(custcols[i]).split(':#:', 2);
|
vals = book.attr(custcols[i]).split(':#:', 2);
|
||||||
|
if (vals[0].indexOf('#T#') == 0) { //startswith
|
||||||
|
vals[0] = vals[0].substr(3, vals[0].length)
|
||||||
|
vals[1] = (vals[1].split(':&:', 2))[1];
|
||||||
|
}
|
||||||
title += '{0}=[{1}] '.format(vals[0], vals[1]);
|
title += '{0}=[{1}] '.format(vals[0], vals[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title += '</span>'
|
||||||
title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id);
|
title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id);
|
||||||
title += '<div class="comments">{0}</div>'.format(comments)
|
title += '<div class="comments">{0}</div>'.format(comments)
|
||||||
// Render authors cell
|
// Render authors cell
|
||||||
@ -170,11 +200,15 @@ function fetch_library_books(start, num, timeout, sort, order, search) {
|
|||||||
var cover = row.find('img').attr('src');
|
var cover = row.find('img').attr('src');
|
||||||
var collapsed = row.find('.comments').css('display') == 'none';
|
var collapsed = row.find('.comments').css('display') == 'none';
|
||||||
$("#book_list tbody tr * .comments").css('display', 'none');
|
$("#book_list tbody tr * .comments").css('display', 'none');
|
||||||
|
$("#book_list tbody tr * .tagdata_short").css('display', 'inherit');
|
||||||
|
$("#book_list tbody tr * .tagdata_long").css('display', 'none');
|
||||||
$('#cover_pane').css('visibility', 'hidden');
|
$('#cover_pane').css('visibility', 'hidden');
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
row.find('.comments').css('display', 'inherit');
|
row.find('.comments').css('display', 'inherit');
|
||||||
$('#cover_pane img').attr('src', cover);
|
$('#cover_pane img').attr('src', cover);
|
||||||
$('#cover_pane').css('visibility', 'visible');
|
$('#cover_pane').css('visibility', 'visible');
|
||||||
|
row.find(".tagdata_short").css('display', 'none');
|
||||||
|
row.find(".tagdata_long").css('display', 'inherit');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,37 @@ save_template_title_series_sorting = 'library_order'
|
|||||||
auto_connect_to_folder = ''
|
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={}
|
||||||
|
|
||||||
|
|
||||||
# Create search terms to apply a query across several built-in search terms.
|
# Create search terms to apply a query across several built-in search terms.
|
||||||
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
|
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
|
||||||
# Example: create the term 'myseries' that when used as myseries:foo would
|
# Example: create the term 'myseries' that when used as myseries:foo would
|
||||||
@ -114,3 +145,4 @@ add_new_book_tags_when_importing_books = False
|
|||||||
# Set the maximum number of tags to show per book in the content server
|
# Set the maximum number of tags to show per book in the content server
|
||||||
max_content_server_tags_shown=5
|
max_content_server_tags_shown=5
|
||||||
|
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ class MetadataReaderPlugin(Plugin): # {{{
|
|||||||
with the input data.
|
with the input data.
|
||||||
:param type: The type of file. Guaranteed to be one of the entries
|
:param type: The type of file. Guaranteed to be one of the entries
|
||||||
in :attr:`file_types`.
|
in :attr:`file_types`.
|
||||||
:return: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
:return: A :class:`calibre.ebooks.metadata.book.Metadata` object
|
||||||
'''
|
'''
|
||||||
return None
|
return None
|
||||||
# }}}
|
# }}}
|
||||||
@ -248,7 +248,7 @@ class MetadataWriterPlugin(Plugin): # {{{
|
|||||||
with the input data.
|
with the input data.
|
||||||
:param type: The type of file. Guaranteed to be one of the entries
|
:param type: The type of file. Guaranteed to be one of the entries
|
||||||
in :attr:`file_types`.
|
in :attr:`file_types`.
|
||||||
:param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
:param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ from calibre.devices.errors import UserFeedback
|
|||||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||||
from calibre.ebooks.metadata import MetaInformation, authors_to_string
|
from calibre.ebooks.metadata import authors_to_string, MetaInformation
|
||||||
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.ebooks.metadata.epub import set_metadata
|
from calibre.ebooks.metadata.epub import set_metadata
|
||||||
from calibre.library.server.utils import strftime
|
from calibre.library.server.utils import strftime
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
@ -871,7 +872,7 @@ class ITUNES(DriverBase):
|
|||||||
once uploaded to the device. len(names) == len(files)
|
once uploaded to the device. len(names) == len(files)
|
||||||
:return: A list of 3-element tuples. The list is meant to be passed
|
:return: A list of 3-element tuples. The list is meant to be passed
|
||||||
to L{add_books_to_metadata}.
|
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
|
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
|
put the book. len(metadata) == len(files). Apart from the regular
|
||||||
cover (path to cover), there may also be a thumbnail attribute, which should
|
cover (path to cover), there may also be a thumbnail attribute, which should
|
||||||
@ -2999,14 +3000,14 @@ class BookList(list):
|
|||||||
'''
|
'''
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
class Book(MetaInformation):
|
class Book(Metadata):
|
||||||
'''
|
'''
|
||||||
A simple class describing a book in the iTunes Books Library.
|
A simple class describing a book in the iTunes Books Library.
|
||||||
- See ebooks.metadata.__init__ for all fields
|
- See ebooks.metadata.__init__ for all fields
|
||||||
'''
|
'''
|
||||||
def __init__(self,title,author):
|
def __init__(self,title,author):
|
||||||
|
|
||||||
MetaInformation.__init__(self, title, authors=[author])
|
Metadata.__init__(self, title, authors=[author])
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def title_sorter(self):
|
def title_sorter(self):
|
||||||
|
@ -316,7 +316,7 @@ class DevicePlugin(Plugin):
|
|||||||
being uploaded to the device.
|
being uploaded to the device.
|
||||||
:param names: A list of file names that the books should have
|
:param names: A list of file names that the books should have
|
||||||
once uploaded to the device. len(names) == len(files)
|
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
|
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
|
put the book. len(metadata) == len(files). Apart from the regular
|
||||||
cover (path to cover), there may also be a thumbnail attribute, which should
|
cover (path to cover), there may also be a thumbnail attribute, which should
|
||||||
@ -335,7 +335,7 @@ class DevicePlugin(Plugin):
|
|||||||
the device.
|
the device.
|
||||||
|
|
||||||
:param locations: Result of a call to L{upload_books}
|
: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`.
|
:meth:`upload_books`.
|
||||||
:param booklists: A tuple containing the result of calls to
|
:param booklists: A tuple containing the result of calls to
|
||||||
(:meth:`books(oncard=None)`,
|
(:meth:`books(oncard=None)`,
|
||||||
|
@ -4,37 +4,15 @@ __copyright__ = '2010, Timothy Legge <timlegge at gmail.com>'
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.devices.usbms.books import Book as Book_
|
||||||
from calibre.constants import filesystem_encoding, preferred_encoding
|
|
||||||
from calibre import isbytestring
|
|
||||||
|
|
||||||
class Book(MetaInformation):
|
class Book(Book_):
|
||||||
|
|
||||||
BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book']
|
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType,
|
||||||
|
thumbnail_name, size=None, other=None):
|
||||||
JSON_ATTRS = [
|
Book_.__init__(self, prefix, lpath)
|
||||||
'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', 'device_collections',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, size=None, other=None):
|
|
||||||
|
|
||||||
MetaInformation.__init__(self, '')
|
|
||||||
self.device_collections = []
|
|
||||||
self._new_book = False
|
|
||||||
|
|
||||||
self.path = os.path.join(prefix, lpath)
|
|
||||||
if os.sep == '\\':
|
|
||||||
self.path = self.path.replace('/', '\\')
|
|
||||||
self.lpath = lpath.replace('\\', '/')
|
|
||||||
else:
|
|
||||||
self.lpath = lpath
|
|
||||||
|
|
||||||
self.title = title
|
self.title = title
|
||||||
if not authors:
|
if not authors:
|
||||||
@ -50,65 +28,14 @@ class Book(MetaInformation):
|
|||||||
else:
|
else:
|
||||||
self.datetime = time.gmtime(os.path.getctime(self.path))
|
self.datetime = time.gmtime(os.path.getctime(self.path))
|
||||||
except:
|
except:
|
||||||
self.datetime = time.gmtime()
|
self.datetime = time.gmtime()
|
||||||
|
if thumbnail_name is not None:
|
||||||
if thumbnail_name is not None:
|
self.thumbnail = ImageWrapper(thumbnail_name)
|
||||||
self.thumbnail = ImageWrapper(thumbnail_name)
|
|
||||||
self.tags = []
|
self.tags = []
|
||||||
if other:
|
if other:
|
||||||
self.smart_update(other)
|
self.smart_update(other)
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.path == getattr(other, 'path', None)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def db_id(self):
|
|
||||||
doc = '''The database id in the application database that this file corresponds to'''
|
|
||||||
def fget(self):
|
|
||||||
match = re.search(r'_(\d+)$', self.lpath.rpartition('.')[0])
|
|
||||||
if match:
|
|
||||||
return int(match.group(1))
|
|
||||||
return None
|
|
||||||
return property(fget=fget, doc=doc)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
def title_sorter(self):
|
|
||||||
doc = '''String to sort the title. If absent, title is returned'''
|
|
||||||
def fget(self):
|
|
||||||
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
|
||||||
return property(doc=doc, fget=fget)
|
|
||||||
|
|
||||||
@dynamic_property
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 ImageWrapper(object):
|
class ImageWrapper(object):
|
||||||
def __init__(self, image_path):
|
def __init__(self, image_path):
|
||||||
self.image_path = image_path
|
self.image_path = image_path
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ class KOBO(USBMS):
|
|||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
for i, row in enumerate(cursor):
|
for i, row in enumerate(cursor):
|
||||||
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
||||||
|
|
||||||
path = self.path_from_contentid(row[3], row[5], oncard)
|
path = self.path_from_contentid(row[3], row[5], oncard)
|
||||||
mime = mime_type_ext(path_to_ext(row[3]))
|
mime = mime_type_ext(path_to_ext(row[3]))
|
||||||
|
@ -6,29 +6,19 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import os, re, time, sys
|
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.mime import mime_type_ext
|
||||||
from calibre.devices.interface import BookList as _BookList
|
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 import isbytestring
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs, tweaks
|
||||||
|
from calibre.utils.date import format_date
|
||||||
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):
|
def __init__(self, prefix, lpath, size=None, other=None):
|
||||||
from calibre.ebooks.metadata.meta import path_to_ext
|
from calibre.ebooks.metadata.meta import path_to_ext
|
||||||
|
|
||||||
MetaInformation.__init__(self, '')
|
Metadata.__init__(self, '')
|
||||||
|
|
||||||
self._new_book = False
|
self._new_book = False
|
||||||
self.device_collections = []
|
self.device_collections = []
|
||||||
@ -72,32 +62,6 @@ class Book(MetaInformation):
|
|||||||
def thumbnail(self):
|
def thumbnail(self):
|
||||||
return None
|
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):
|
class BookList(_BookList):
|
||||||
|
|
||||||
def __init__(self, oncard, prefix, settings):
|
def __init__(self, oncard, prefix, settings):
|
||||||
@ -131,11 +95,38 @@ class CollectionsBookList(BookList):
|
|||||||
def supports_collections(self):
|
def supports_collections(self):
|
||||||
return True
|
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):
|
def get_collections(self, collection_attributes):
|
||||||
from calibre.devices.usbms.driver import debug_print
|
from calibre.devices.usbms.driver import debug_print
|
||||||
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
|
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
|
||||||
|
debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
|
||||||
collections = {}
|
collections = {}
|
||||||
series_categories = set([])
|
|
||||||
# This map of sets is used to avoid linear searches when testing for
|
# This map of sets is used to avoid linear searches when testing for
|
||||||
# book equality
|
# book equality
|
||||||
collections_lpaths = {}
|
collections_lpaths = {}
|
||||||
@ -161,41 +152,55 @@ class CollectionsBookList(BookList):
|
|||||||
# For existing books, modify the collections only if the user
|
# For existing books, modify the collections only if the user
|
||||||
# specified 'on_connect'
|
# specified 'on_connect'
|
||||||
attrs = collection_attributes
|
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:
|
for attr in attrs:
|
||||||
attr = attr.strip()
|
attr = attr.strip()
|
||||||
val = getattr(book, attr, None)
|
val = meta_vals.get(attr, None)
|
||||||
if not val: continue
|
if not val: continue
|
||||||
if isbytestring(val):
|
if isbytestring(val):
|
||||||
val = val.decode(preferred_encoding, 'replace')
|
val = val.decode(preferred_encoding, 'replace')
|
||||||
if isinstance(val, (list, tuple)):
|
if isinstance(val, (list, tuple)):
|
||||||
val = list(val)
|
val = list(val)
|
||||||
elif isinstance(val, unicode):
|
else:
|
||||||
val = [val]
|
val = [val]
|
||||||
for category in val:
|
for category in val:
|
||||||
if attr == 'tags' and len(category) > 1 and \
|
is_series = False
|
||||||
category[0] == '[' and category[-1] == ']':
|
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
|
continue
|
||||||
if category not in collections:
|
collections_lpaths[cat_name].add(lpath)
|
||||||
collections[category] = []
|
if is_series:
|
||||||
collections_lpaths[category] = set()
|
collections[cat_name].append(
|
||||||
if lpath not in collections_lpaths[category]:
|
(book, meta_vals.get(attr+'_index', sys.maxint)))
|
||||||
collections_lpaths[category].add(lpath)
|
else:
|
||||||
collections[category].append(book)
|
collections[cat_name].append(
|
||||||
if attr == 'series' or \
|
(book, meta_vals.get('title_sort', 'zzzz')))
|
||||||
('series' in collection_attributes and
|
|
||||||
getattr(book, 'series', None) == category):
|
|
||||||
series_categories.add(category)
|
|
||||||
# Sort collections
|
# Sort collections
|
||||||
|
result = {}
|
||||||
for category, books in collections.items():
|
for category, books in collections.items():
|
||||||
def tgetter(x):
|
books.sort(cmp=lambda x,y:cmp(x[1], y[1]))
|
||||||
return getattr(x, 'title_sort', 'zzzz')
|
result[category] = [x[0] for x in books]
|
||||||
books.sort(cmp=lambda x,y:cmp(tgetter(x), tgetter(y)))
|
return result
|
||||||
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
|
|
||||||
|
|
||||||
def rebuild_collections(self, booklist, oncard):
|
def rebuild_collections(self, booklist, oncard):
|
||||||
'''
|
'''
|
||||||
|
@ -13,7 +13,6 @@ for a particular device.
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import json
|
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
|
||||||
from calibre import prints, isbytestring
|
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.cli import CLI
|
||||||
from calibre.devices.usbms.device import Device
|
from calibre.devices.usbms.device import Device
|
||||||
from calibre.devices.usbms.books import BookList, Book
|
from calibre.devices.usbms.books import BookList, Book
|
||||||
|
from calibre.ebooks.metadata.book.json_codec import JsonCodec
|
||||||
|
|
||||||
BASE_TIME = None
|
BASE_TIME = None
|
||||||
def debug_print(*args):
|
def debug_print(*args):
|
||||||
@ -288,6 +288,7 @@ class USBMS(CLI, Device):
|
|||||||
# at the end just before the return
|
# at the end just before the return
|
||||||
def sync_booklists(self, booklists, end_session=True):
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
debug_print('USBMS: starting sync_booklists')
|
debug_print('USBMS: starting sync_booklists')
|
||||||
|
json_codec = JsonCodec()
|
||||||
|
|
||||||
if not os.path.exists(self.normalize_path(self._main_prefix)):
|
if not os.path.exists(self.normalize_path(self._main_prefix)):
|
||||||
os.makedirs(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 prefix is not None and isinstance(booklists[listid], self.booklist_class):
|
||||||
if not os.path.exists(prefix):
|
if not os.path.exists(prefix):
|
||||||
os.makedirs(self.normalize_path(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:
|
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._main_prefix, 0)
|
||||||
write_prefix(self._card_a_prefix, 1)
|
write_prefix(self._card_a_prefix, 1)
|
||||||
write_prefix(self._card_b_prefix, 2)
|
write_prefix(self._card_b_prefix, 2)
|
||||||
@ -345,19 +344,13 @@ class USBMS(CLI, Device):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_metadata_cache(cls, bl, prefix, name):
|
def parse_metadata_cache(cls, bl, prefix, name):
|
||||||
# bl = cls.booklist_class()
|
json_codec = JsonCodec()
|
||||||
js = []
|
|
||||||
need_sync = False
|
need_sync = False
|
||||||
cache_file = cls.normalize_path(os.path.join(prefix, name))
|
cache_file = cls.normalize_path(os.path.join(prefix, name))
|
||||||
if os.access(cache_file, os.R_OK):
|
if os.access(cache_file, os.R_OK):
|
||||||
try:
|
try:
|
||||||
with open(cache_file, 'rb') as f:
|
with open(cache_file, 'rb') as f:
|
||||||
js = json.load(f, encoding='utf-8')
|
json_codec.decode_from_file(f, bl, cls.book_class, prefix)
|
||||||
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)
|
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -392,7 +385,7 @@ class USBMS(CLI, Device):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def book_from_path(cls, prefix, lpath):
|
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:
|
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
|
||||||
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
|
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)),
|
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
|
||||||
cls.build_template_regexp())
|
cls.build_template_regexp())
|
||||||
if mi is None:
|
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')])
|
[_('Unknown')])
|
||||||
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
|
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
|
||||||
book = cls.book_class(prefix, lpath, other=mi, size=size)
|
book = cls.book_class(prefix, lpath, other=mi, size=size)
|
||||||
|
@ -10,10 +10,9 @@ import os, mimetypes, sys, re
|
|||||||
from urllib import unquote, quote
|
from urllib import unquote, quote
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from calibre import relpath, prints
|
from calibre import relpath
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import isoformat
|
|
||||||
|
|
||||||
_author_pat = re.compile(',?\s+(and|with)\s+', re.IGNORECASE)
|
_author_pat = re.compile(',?\s+(and|with)\s+', re.IGNORECASE)
|
||||||
def string_to_authors(raw):
|
def string_to_authors(raw):
|
||||||
@ -221,214 +220,18 @@ class ResourceCollection(object):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MetaInformation(object):
|
def MetaInformation(title, authors=(_('Unknown'),)):
|
||||||
'''Convenient encapsulation of book metadata'''
|
''' Convenient encapsulation of book metadata, needed for compatibility
|
||||||
|
|
||||||
@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'),)):
|
|
||||||
'''
|
|
||||||
@param title: title or ``_('Unknown')`` or a MetaInformation object
|
@param title: title or ``_('Unknown')`` or a MetaInformation object
|
||||||
@param authors: List of strings or []
|
@param authors: List of strings or []
|
||||||
'''
|
'''
|
||||||
mi = None
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
if hasattr(title, 'title') and hasattr(title, 'authors'):
|
mi = None
|
||||||
mi = title
|
if hasattr(title, 'title') and hasattr(title, 'authors'):
|
||||||
title = mi.title
|
mi = title
|
||||||
authors = mi.authors
|
title = mi.title
|
||||||
self.title = title
|
authors = mi.authors
|
||||||
self.author = list(authors) if authors else []# Needed for backward compatibility
|
return Metadata(title, authors, mi)
|
||||||
#: 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'<tr><td><b>%s</b></td><td>%s</td></tr>'%x
|
|
||||||
return u'<table>%s</table>'%u'\n'.join(ans)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__unicode__().encode('utf-8')
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
return bool(self.title or self.author or self.comments or self.tags)
|
|
||||||
|
|
||||||
def check_isbn10(isbn):
|
def check_isbn10(isbn):
|
||||||
try:
|
try:
|
||||||
|
@ -11,48 +11,45 @@ an empty list/dictionary for complex types and (None, None) for cover_data
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
SOCIAL_METADATA_FIELDS = frozenset([
|
SOCIAL_METADATA_FIELDS = frozenset([
|
||||||
'tags', # Ordered list
|
'tags', # Ordered list
|
||||||
# A floating point number between 0 and 10
|
'rating', # A floating point number between 0 and 10
|
||||||
'rating',
|
'comments', # A simple HTML enabled string
|
||||||
# A simple HTML enabled string
|
'series', # A simple string
|
||||||
'comments',
|
'series_index', # A floating point number
|
||||||
# A simple string
|
|
||||||
'series',
|
|
||||||
# A floating point number
|
|
||||||
'series_index',
|
|
||||||
# Of the form { scheme1:value1, scheme2:value2}
|
# Of the form { scheme1:value1, scheme2:value2}
|
||||||
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
|
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
|
||||||
'classifiers',
|
'classifiers',
|
||||||
'isbn', # Pseudo field for convenience, should get/set isbn classifier
|
])
|
||||||
|
|
||||||
|
'''
|
||||||
|
The list of names that convert to classifiers when in get and set.
|
||||||
|
'''
|
||||||
|
|
||||||
|
TOP_LEVEL_CLASSIFIERS = frozenset([
|
||||||
|
'isbn',
|
||||||
])
|
])
|
||||||
|
|
||||||
PUBLICATION_METADATA_FIELDS = frozenset([
|
PUBLICATION_METADATA_FIELDS = frozenset([
|
||||||
# title must never be None. Should be _('Unknown')
|
'title', # title must never be None. Should be _('Unknown')
|
||||||
'title',
|
|
||||||
# Pseudo field that can be set, but if not set is auto generated
|
# Pseudo field that can be set, but if not set is auto generated
|
||||||
# from title and languages
|
# from title and languages
|
||||||
'title_sort',
|
'title_sort',
|
||||||
# Ordered list of authors. Must never be None, can be [_('Unknown')]
|
'authors', # Ordered list. Must never be None, can be [_('Unknown')]
|
||||||
'authors',
|
'author_sort_map', # Map of sort strings for each author
|
||||||
# Map of sort strings for each author
|
|
||||||
'author_sort_map',
|
|
||||||
# Pseudo field that can be set, but if not set is auto generated
|
# Pseudo field that can be set, but if not set is auto generated
|
||||||
# from authors and languages
|
# from authors and languages
|
||||||
'author_sort',
|
'author_sort',
|
||||||
'book_producer',
|
'book_producer',
|
||||||
# Dates and times must be timezone aware
|
'timestamp', # Dates and times must be timezone aware
|
||||||
'timestamp',
|
|
||||||
'pubdate',
|
'pubdate',
|
||||||
'rights',
|
'rights',
|
||||||
# So far only known publication type is periodical:calibre
|
# So far only known publication type is periodical:calibre
|
||||||
# If None, means book
|
# If None, means book
|
||||||
'publication_type',
|
'publication_type',
|
||||||
# A UUID usually of type 4
|
'uuid', # A UUID usually of type 4
|
||||||
'uuid',
|
'language', # the primary language of this book
|
||||||
'languages', # ordered list
|
'languages', # ordered list
|
||||||
# Simple string, no special semantics
|
'publisher', # Simple string, no special semantics
|
||||||
'publisher',
|
|
||||||
# Absolute path to image file encoded in filesystem_encoding
|
# Absolute path to image file encoded in filesystem_encoding
|
||||||
'cover',
|
'cover',
|
||||||
# Of the form (format, data) where format is, for e.g. 'jpeg', 'png', 'gif'...
|
# Of the form (format, data) where format is, for e.g. 'jpeg', 'png', 'gif'...
|
||||||
@ -69,33 +66,56 @@ BOOK_STRUCTURE_FIELDS = frozenset([
|
|||||||
])
|
])
|
||||||
|
|
||||||
USER_METADATA_FIELDS = frozenset([
|
USER_METADATA_FIELDS = frozenset([
|
||||||
# A dict of a form to be specified
|
# A dict of dicts similar to field_metadata. Each field description dict
|
||||||
|
# also contains a value field with the key #value#.
|
||||||
'user_metadata',
|
'user_metadata',
|
||||||
])
|
])
|
||||||
|
|
||||||
DEVICE_METADATA_FIELDS = frozenset([
|
DEVICE_METADATA_FIELDS = frozenset([
|
||||||
# Ordered list of strings
|
'device_collections', # Ordered list of strings
|
||||||
'device_collections',
|
'lpath', # Unicode, / separated
|
||||||
'lpath', # Unicode, / separated
|
'size', # In bytes
|
||||||
# In bytes
|
'mime', # Mimetype of the book file being represented
|
||||||
'size',
|
|
||||||
# Mimetype of the book file being represented
|
|
||||||
'mime',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
CALIBRE_METADATA_FIELDS = frozenset([
|
CALIBRE_METADATA_FIELDS = frozenset([
|
||||||
# An application id
|
'application_id', # An application id, currently set to the db_id.
|
||||||
# Semantics to be defined. Is it a db key? a db name + key? A uuid?
|
# the calibre primary key of the item.
|
||||||
'application_id',
|
'db_id', # the calibre primary key of the item.
|
||||||
|
# TODO: NEWMETA: May want to remove once Sony's no longer use it
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ALL_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
|
PUBLICATION_METADATA_FIELDS).union(
|
||||||
|
BOOK_STRUCTURE_FIELDS).union(
|
||||||
|
USER_METADATA_FIELDS).union(
|
||||||
|
DEVICE_METADATA_FIELDS).union(
|
||||||
|
CALIBRE_METADATA_FIELDS)
|
||||||
|
|
||||||
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
# All fields except custom fields
|
||||||
USER_METADATA_FIELDS).union(
|
STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
PUBLICATION_METADATA_FIELDS).union(
|
PUBLICATION_METADATA_FIELDS).union(
|
||||||
CALIBRE_METADATA_FIELDS).union(
|
BOOK_STRUCTURE_FIELDS).union(
|
||||||
frozenset(['lpath'])) # I don't think we need device_collections
|
DEVICE_METADATA_FIELDS).union(
|
||||||
|
CALIBRE_METADATA_FIELDS)
|
||||||
|
|
||||||
# Serialization of covers/thumbnails will have to be handled carefully, maybe
|
# Metadata fields that smart update should copy without special handling
|
||||||
# as an option to the serializer class
|
COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
|
PUBLICATION_METADATA_FIELDS).union(
|
||||||
|
BOOK_STRUCTURE_FIELDS).union(
|
||||||
|
DEVICE_METADATA_FIELDS).union(
|
||||||
|
CALIBRE_METADATA_FIELDS) - \
|
||||||
|
frozenset(['title', 'title_sort', 'authors',
|
||||||
|
'author_sort', 'author_sort_map' 'comments',
|
||||||
|
'cover_data', 'tags', 'language', 'lpath',
|
||||||
|
'size'])
|
||||||
|
|
||||||
|
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
|
USER_METADATA_FIELDS).union(
|
||||||
|
PUBLICATION_METADATA_FIELDS).union(
|
||||||
|
CALIBRE_METADATA_FIELDS).union(
|
||||||
|
DEVICE_METADATA_FIELDS) - \
|
||||||
|
frozenset(['device_collections'])
|
||||||
|
# device_collections is rebuilt when needed
|
||||||
|
@ -6,8 +6,15 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import copy
|
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 = {
|
NULL_VALUES = {
|
||||||
'user_metadata': {},
|
'user_metadata': {},
|
||||||
@ -19,103 +26,356 @@ NULL_VALUES = {
|
|||||||
'author_sort_map': {},
|
'author_sort_map': {},
|
||||||
'authors' : [_('Unknown')],
|
'authors' : [_('Unknown')],
|
||||||
'title' : _('Unknown'),
|
'title' : _('Unknown'),
|
||||||
|
'language' : 'und'
|
||||||
}
|
}
|
||||||
|
|
||||||
class Metadata(object):
|
class Metadata(object):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
This class must expose a superset of the API of MetaInformation in terms
|
A class representing all the metadata for a book.
|
||||||
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.
|
|
||||||
|
|
||||||
Please keep the method based API of this class to a minimum. Every method
|
Please keep the method based API of this class to a minimum. Every method
|
||||||
becomes a reserved field name.
|
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))
|
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):
|
def __getattribute__(self, field):
|
||||||
_data = object.__getattribute__(self, '_data')
|
_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)
|
return _data.get(field, None)
|
||||||
try:
|
try:
|
||||||
return object.__getattribute__(self, field)
|
return object.__getattribute__(self, field)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
if field in _data['user_metadata'].iterkeys():
|
if field in _data['user_metadata'].iterkeys():
|
||||||
# TODO: getting user metadata values
|
return _data['user_metadata'][field]['#value#']
|
||||||
pass
|
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'Metadata object has no attribute named: '+ repr(field))
|
'Metadata object has no attribute named: '+ repr(field))
|
||||||
|
|
||||||
|
def __setattr__(self, field, val, extra=None):
|
||||||
def __setattr__(self, field, val):
|
|
||||||
_data = object.__getattribute__(self, '_data')
|
_data = object.__getattribute__(self, '_data')
|
||||||
if field in RESERVED_METADATA_FIELDS:
|
if field in TOP_LEVEL_CLASSIFIERS:
|
||||||
if field != 'user_metadata':
|
_data['classifiers'].update({field: val})
|
||||||
if not val:
|
elif field in STANDARD_METADATA_FIELDS:
|
||||||
val = NULL_VALUES[field]
|
if val is None:
|
||||||
_data[field] = val
|
val = NULL_VALUES.get(field, None)
|
||||||
else:
|
_data[field] = val
|
||||||
raise AttributeError('You cannot set user_metadata directly.')
|
|
||||||
elif field in _data['user_metadata'].iterkeys():
|
elif field in _data['user_metadata'].iterkeys():
|
||||||
# TODO: Setting custom column values
|
_data['user_metadata'][field]['#value#'] = val
|
||||||
pass
|
_data['user_metadata'][field]['#extra#'] = extra
|
||||||
else:
|
else:
|
||||||
# You are allowed to stick arbitrary attributes onto this object as
|
# 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
|
# Don't abuse this privilege
|
||||||
self.__dict__[field] = val
|
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
|
@property
|
||||||
def user_metadata_names(self):
|
def user_metadata_keys(self):
|
||||||
'The set of user metadata names this object knows about'
|
'The set of user metadata names this object knows about'
|
||||||
_data = object.__getattribute__(self, '_data')
|
_data = object.__getattribute__(self, '_data')
|
||||||
return frozenset(_data['user_metadata'].iterkeys())
|
return frozenset(_data['user_metadata'].iterkeys())
|
||||||
|
|
||||||
# Old MetaInformation API {{{
|
def get_all_user_metadata(self, make_copy):
|
||||||
def copy(self):
|
'''
|
||||||
pass
|
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):
|
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):
|
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):
|
if other.title and other.title != _('Unknown'):
|
||||||
pass
|
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):
|
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):
|
def format_authors(self):
|
||||||
pass
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
|
return authors_to_string(self.authors)
|
||||||
|
|
||||||
def format_tags(self):
|
def format_tags(self):
|
||||||
pass
|
return u', '.join([unicode(t) for t in self.tags])
|
||||||
|
|
||||||
def format_rating(self):
|
def format_rating(self):
|
||||||
return unicode(self.rating)
|
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):
|
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):
|
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'<tr><td><b>%s</b></td><td>%s</td></tr>'%x
|
||||||
|
return u'<table>%s</table>'%u'\n'.join(ans)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__unicode__().encode('utf-8')
|
return self.__unicode__().encode('utf-8')
|
||||||
|
|
||||||
def __nonzero__(self):
|
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
|
|
||||||
|
|
||||||
|
124
src/calibre/ebooks/metadata/book/json_codec.py
Normal file
124
src/calibre/ebooks/metadata/book/json_codec.py
Normal file
@ -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
|
@ -164,7 +164,7 @@ def get_cover(opf, opf_path, stream, reader=None):
|
|||||||
return render_html_svg_workaround(cpage, default_log)
|
return render_html_svg_workaround(cpage, default_log)
|
||||||
|
|
||||||
def get_metadata(stream, extract_cover=True):
|
def get_metadata(stream, extract_cover=True):
|
||||||
""" Return metadata as a :class:`MetaInformation` object """
|
""" Return metadata as a :class:`Metadata` object """
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
reader = OCFZipReader(stream)
|
reader = OCFZipReader(stream)
|
||||||
mi = MetaInformation(reader.opf)
|
mi = MetaInformation(reader.opf)
|
||||||
|
@ -29,7 +29,7 @@ class MetadataSource(Plugin): # {{{
|
|||||||
future use.
|
future use.
|
||||||
|
|
||||||
The fetch method must store the results in `self.results` as a list of
|
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).
|
in `self.exception` and `self.tb` (for the traceback).
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import sys, re
|
|||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
from calibre.utils.config import OptionParser
|
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.ebooks.BeautifulSoup import BeautifulStoneSoup
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
|
|
||||||
@ -42,10 +42,10 @@ def fetch_metadata(url, max=100, timeout=5.):
|
|||||||
return books
|
return books
|
||||||
|
|
||||||
|
|
||||||
class ISBNDBMetadata(MetaInformation):
|
class ISBNDBMetadata(Metadata):
|
||||||
|
|
||||||
def __init__(self, book):
|
def __init__(self, book):
|
||||||
MetaInformation.__init__(self, None, [])
|
Metadata.__init__(self, None, [])
|
||||||
|
|
||||||
self.isbn = book.get('isbn13', book.get('isbn'))
|
self.isbn = book.get('isbn13', book.get('isbn'))
|
||||||
self.title = book.find('titlelong').string
|
self.title = book.find('titlelong').string
|
||||||
|
@ -16,7 +16,8 @@ from lxml import etree
|
|||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.constants import __appname__, __version__, filesystem_encoding
|
from calibre.constants import __appname__, __version__, filesystem_encoding
|
||||||
from calibre.ebooks.metadata.toc import TOC
|
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.date import parse_date, isoformat
|
||||||
from calibre.utils.localization import get_lang
|
from calibre.utils.localization import get_lang
|
||||||
|
|
||||||
@ -926,16 +927,16 @@ class OPF(object):
|
|||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
|
||||||
|
|
||||||
class OPFCreator(MetaInformation):
|
class OPFCreator(Metadata):
|
||||||
|
|
||||||
def __init__(self, base_path, *args, **kwargs):
|
def __init__(self, base_path, other):
|
||||||
'''
|
'''
|
||||||
Initialize.
|
Initialize.
|
||||||
@param base_path: An absolute path to the directory in which this OPF file
|
@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
|
will eventually be. This is used by the L{create_manifest} method
|
||||||
to convert paths to files into relative paths.
|
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)
|
self.base_path = os.path.abspath(base_path)
|
||||||
if self.application_id is None:
|
if self.application_id is None:
|
||||||
self.application_id = str(uuid.uuid4())
|
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')
|
factory(DC('contributor'), mi.book_producer, __appname__, 'bkp')
|
||||||
if hasattr(mi.pubdate, 'isoformat'):
|
if hasattr(mi.pubdate, 'isoformat'):
|
||||||
factory(DC('date'), isoformat(mi.pubdate))
|
factory(DC('date'), isoformat(mi.pubdate))
|
||||||
if mi.category:
|
if hasattr(mi, 'category') and mi.category:
|
||||||
factory(DC('type'), mi.category)
|
factory(DC('type'), mi.category)
|
||||||
if mi.comments:
|
if mi.comments:
|
||||||
factory(DC('description'), mi.comments)
|
factory(DC('description'), mi.comments)
|
||||||
|
@ -28,6 +28,8 @@ WEIGHTS[_('Tags')] = 4
|
|||||||
|
|
||||||
def render_rows(data):
|
def render_rows(data):
|
||||||
keys = data.keys()
|
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]))
|
keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y]))
|
||||||
rows = []
|
rows = []
|
||||||
for key in keys:
|
for key in keys:
|
||||||
|
@ -323,7 +323,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
data[_('Series')] = \
|
data[_('Series')] = \
|
||||||
_('Book <font face="serif">%s</font> of %s.')%\
|
_('Book <font face="serif">%s</font> of %s.')%\
|
||||||
(sidx, prepare_string_for_xml(series))
|
(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
|
return data
|
||||||
|
|
||||||
def set_cache(self, idx):
|
def set_cache(self, idx):
|
||||||
@ -372,7 +376,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
|
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 = [], []
|
metadata, _full_metadata = [], []
|
||||||
if not rows_are_ids:
|
if not rows_are_ids:
|
||||||
rows = [self.db.id(row.row()) for row in rows]
|
rows = [self.db.id(row.row()) for row in rows]
|
||||||
@ -1053,7 +1056,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
if hasattr(cdata, 'image_path'):
|
if hasattr(cdata, 'image_path'):
|
||||||
img.load(cdata.image_path)
|
img.load(cdata.image_path)
|
||||||
else:
|
else:
|
||||||
img.loadFromData(cdata)
|
img.loadFromData(cdata[2])
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
img = self.default_image
|
img = self.default_image
|
||||||
data['cover'] = img
|
data['cover'] = img
|
||||||
|
@ -8,10 +8,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from PyQt4.Qt import QWidget, pyqtSignal
|
from PyQt4.Qt import QWidget, pyqtSignal
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
|
||||||
from calibre.gui2.preferences.save_template_ui import Ui_Form
|
from calibre.gui2.preferences.save_template_ui import Ui_Form
|
||||||
from calibre.library.save_to_disk import FORMAT_ARG_DESCS, \
|
from calibre.library.save_to_disk import FORMAT_ARG_DESCS
|
||||||
preprocess_template
|
|
||||||
|
|
||||||
class SaveTemplate(QWidget, Ui_Form):
|
class SaveTemplate(QWidget, Ui_Form):
|
||||||
|
|
||||||
@ -41,18 +39,21 @@ class SaveTemplate(QWidget, Ui_Form):
|
|||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
tmpl = preprocess_template(self.opt_template.text())
|
# TODO: NEWMETA: I haven't figured out how to get the custom columns
|
||||||
fa = {}
|
# into here, so for the moment make all templates valid.
|
||||||
for x in FORMAT_ARG_DESCS.keys():
|
|
||||||
fa[x]='random long string'
|
|
||||||
try:
|
|
||||||
tmpl.format(**fa)
|
|
||||||
except Exception, err:
|
|
||||||
error_dialog(self, _('Invalid template'),
|
|
||||||
'<p>'+_('The template %s is invalid:')%tmpl + \
|
|
||||||
'<br>'+str(err), show=True)
|
|
||||||
return False
|
|
||||||
return True
|
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'),
|
||||||
|
# '<p>'+_('The template %s is invalid:')%tmpl + \
|
||||||
|
# '<br>'+str(err), show=True)
|
||||||
|
# return False
|
||||||
|
# return True
|
||||||
|
|
||||||
def set_value(self, val):
|
def set_value(self, val):
|
||||||
self.opt_template.set_value(val)
|
self.opt_template.set_value(val)
|
||||||
|
@ -521,15 +521,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
'''
|
'''
|
||||||
Convenience method to return metadata as a L{MetaInformation} object.
|
Convenience method to return metadata as a L{MetaInformation} object.
|
||||||
'''
|
'''
|
||||||
aum = self.authors(idx, index_is_id=index_is_id)
|
aut_list = self.authors_with_sort_strings(idx, index_is_id=index_is_id)
|
||||||
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
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 = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
|
||||||
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
|
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
|
||||||
if mi.authors:
|
mi.author_sort_map = aus
|
||||||
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.comments = self.comments(idx, index_is_id=index_is_id)
|
mi.comments = self.comments(idx, index_is_id=index_is_id)
|
||||||
mi.publisher = self.publisher(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)
|
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)
|
mi.isbn = self.isbn(idx, index_is_id=index_is_id)
|
||||||
id = idx if index_is_id else self.id(idx)
|
id = idx if index_is_id else self.id(idx)
|
||||||
mi.application_id = id
|
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:
|
if get_cover:
|
||||||
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
||||||
return mi
|
return mi
|
||||||
@ -1084,6 +1091,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if getattr(mi, 'timestamp', None) is not None:
|
if getattr(mi, 'timestamp', None) is not None:
|
||||||
doit(self.set_timestamp, id, mi.timestamp, notify=False)
|
doit(self.set_timestamp, id, mi.timestamp, notify=False)
|
||||||
self.set_path(id, True)
|
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])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
# Given a book, return the list of author sort strings for the book's authors
|
# 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)
|
result.append(sort)
|
||||||
return result
|
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
|
# Given a book, return the author_sort string for authors of the book
|
||||||
def author_sort_from_book(self, id, index_is_id=False):
|
def author_sort_from_book(self, id, index_is_id=False):
|
||||||
auts = self.authors_sort_strings(id, index_is_id)
|
auts = self.authors_sort_strings(id, index_is_id)
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__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.config import Config, StringConfig, tweaks
|
||||||
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
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.opf2 import metadata_to_opf
|
||||||
from calibre.ebooks.metadata.meta import set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||||
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
|
|
||||||
@ -97,29 +98,33 @@ def preprocess_template(template):
|
|||||||
template = template.decode(preferred_encoding, 'replace')
|
template = template.decode(preferred_encoding, 'replace')
|
||||||
return template
|
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):
|
def safe_format(x, format_args):
|
||||||
try:
|
ans = safe_formatter.vformat(x, [], format_args).strip()
|
||||||
ans = x.format(**format_args).strip()
|
return re.sub(r'\s+', ' ', ans)
|
||||||
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 ''
|
|
||||||
|
|
||||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||||
to_lowercase=False):
|
to_lowercase=False):
|
||||||
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
|
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
|
||||||
tsfmt = title_sort if library_order else lambda x: x
|
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:
|
if mi.title:
|
||||||
format_args['title'] = tsfmt(mi.title)
|
format_args['title'] = tsfmt(mi.title)
|
||||||
if mi.authors:
|
if mi.authors:
|
||||||
format_args['authors'] = mi.format_authors()
|
format_args['authors'] = mi.format_authors()
|
||||||
format_args['author'] = format_args['authors']
|
format_args['author'] = format_args['authors']
|
||||||
if mi.author_sort:
|
|
||||||
format_args['author_sort'] = mi.author_sort
|
|
||||||
if mi.tags:
|
if mi.tags:
|
||||||
format_args['tags'] = mi.format_tags()
|
format_args['tags'] = mi.format_tags()
|
||||||
if format_args['tags'].startswith('/'):
|
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)
|
template = re.sub(r'\{series_index[^}]*?\}', '', template)
|
||||||
if mi.rating is not None:
|
if mi.rating is not None:
|
||||||
format_args['rating'] = mi.format_rating()
|
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'):
|
if hasattr(mi.timestamp, 'timetuple'):
|
||||||
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
|
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
|
||||||
if hasattr(mi.pubdate, 'timetuple'):
|
if hasattr(mi.pubdate, 'timetuple'):
|
||||||
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
|
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
|
||||||
format_args['id'] = str(id)
|
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 = [x.strip() for x in template.split('/') if x.strip()]
|
||||||
components = [safe_format(x, format_args) for x in components]
|
components = [safe_format(x, format_args) for x in components]
|
||||||
components = [sanitize_func(x) for x in components if x]
|
components = [sanitize_func(x) for x in components if x]
|
||||||
|
@ -199,6 +199,10 @@ class MobileServer(object):
|
|||||||
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
|
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
|
||||||
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
|
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
|
||||||
CFM[y]['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 = []
|
books = []
|
||||||
for record in items[(start-1):(start-1)+num]:
|
for record in items[(start-1):(start-1)+num]:
|
||||||
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
|
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import time
|
import time, sys
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
@ -44,8 +44,8 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
|
|||||||
except:
|
except:
|
||||||
return _strftime(fmt, nowf().timetuple())
|
return _strftime(fmt, nowf().timetuple())
|
||||||
|
|
||||||
def format_tag_string(tags, sep):
|
def format_tag_string(tags, sep, ignore_max=False):
|
||||||
MAX = tweaks['max_content_server_tags_shown']
|
MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
|
||||||
if tags:
|
if tags:
|
||||||
tlist = [t.strip() for t in tags.split(sep)]
|
tlist = [t.strip() for t in tags.split(sep)]
|
||||||
else:
|
else:
|
||||||
@ -53,5 +53,6 @@ def format_tag_string(tags, sep):
|
|||||||
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
if len(tlist) > MAX:
|
if len(tlist) > MAX:
|
||||||
tlist = 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 ''
|
||||||
|
|
||||||
|
@ -66,6 +66,10 @@ class XMLServer(object):
|
|||||||
return x.decode(preferred_encoding, 'replace')
|
return x.decode(preferred_encoding, 'replace')
|
||||||
return unicode(x)
|
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]:
|
for record in items[start:start+num]:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
||||||
@ -85,7 +89,7 @@ class XMLServer(object):
|
|||||||
'comments'):
|
'comments'):
|
||||||
y = record[FM[x]]
|
y = record[FM[x]]
|
||||||
if x == 'tags':
|
if x == 'tags':
|
||||||
y = format_tag_string(y, ',')
|
y = format_tag_string(y, ',', ignore_max=True)
|
||||||
kwargs[x] = serialize(y) if y else ''
|
kwargs[x] = serialize(y) if y else ''
|
||||||
|
|
||||||
c = kwargs.pop('comments')
|
c = kwargs.pop('comments')
|
||||||
@ -107,7 +111,9 @@ class XMLServer(object):
|
|||||||
name = CFM[key]['name']
|
name = CFM[key]['name']
|
||||||
custcols.append(k)
|
custcols.append(k)
|
||||||
if datatype == 'text' and CFM[key]['is_multiple']:
|
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':
|
elif datatype == 'series':
|
||||||
kwargs[k] = concat(name, '%s [%s]'%(val,
|
kwargs[k] = concat(name, '%s [%s]'%(val,
|
||||||
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
|
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,
|
return etree.tostring(ans, encoding='utf-8', pretty_print=True,
|
||||||
xml_declaration=True)
|
xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user