Implement user_metadata, user_categories and author_link_map

This commit is contained in:
Kovid Goyal 2016-06-23 20:37:08 +05:30
parent a9aa87bf50
commit 857dad038f
2 changed files with 148 additions and 2 deletions

View File

@ -7,14 +7,17 @@ from __future__ import (unicode_literals, division, absolute_import,
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from functools import wraps from functools import wraps
from future_builtins import map from future_builtins import map
import re import re, json
from lxml import etree from lxml import etree
from calibre import prints
from calibre.ebooks.metadata import check_isbn, authors_to_string, string_to_authors from calibre.ebooks.metadata import check_isbn, authors_to_string, string_to_authors
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.json_codec import object_to_unicode, decode_is_multiple, encode_is_multiple
from calibre.ebooks.metadata.utils import parse_opf, pretty_print_opf, ensure_unique, normalize_languages from calibre.ebooks.metadata.utils import parse_opf, pretty_print_opf, ensure_unique, normalize_languages
from calibre.ebooks.oeb.base import OPF2_NSMAP, OPF, DC from calibre.ebooks.oeb.base import OPF2_NSMAP, OPF, DC
from calibre.utils.config import from_json, to_json
from calibre.utils.date import parse_date as parse_date_, fix_only_date, is_date_undefined, isoformat from calibre.utils.date import parse_date as parse_date_, fix_only_date, is_date_undefined, isoformat
from calibre.utils.iso8601 import parse_iso8601 from calibre.utils.iso8601 import parse_iso8601
from calibre.utils.localization import canonicalize_lang from calibre.utils.localization import canonicalize_lang
@ -30,6 +33,9 @@ def uniq(vals):
seen_add = seen.add seen_add = seen.add
return list(x for x in vals if x not in seen and not seen_add(x)) return list(x for x in vals if x not in seen and not seen_add(x))
def dump_dict(cats):
return json.dumps(object_to_unicode(cats or {}), ensure_ascii=False, skipkeys=True)
def XPath(x): def XPath(x):
try: try:
return _xpath_cache[x] return _xpath_cache[x]
@ -666,6 +672,109 @@ def set_series(root, prefixes, refines, series, series_index):
set_refines(d, refines, refdef('collection-type', 'series'), refdef('group-position', '%.2g' % series_index)) set_refines(d, refines, refdef('collection-type', 'series'), refdef('group-position', '%.2g' % series_index))
# }}} # }}}
# User metadata {{{
def dict_reader(name, load=json.loads, try2=True):
pq = '%s:%s' % (CALIBRE_PREFIX, name)
def reader(root, prefixes, refines):
for meta in XPath('./opf:metadata/opf:meta[@property]')(root):
val = (meta.text or '').strip()
if val:
prop = expand_prefix(meta.get('property'), prefixes)
if prop.lower() == pq:
try:
ans = load(val)
if isinstance(ans, dict):
return ans
except Exception:
continue
if try2:
for meta in XPath('./opf:metadata/opf:meta[@name="calibre:%s"]' % name)(root):
val = meta.get('content')
if val:
try:
ans = load(val)
if isinstance(ans, dict):
return ans
except Exception:
continue
return reader
read_user_categories = dict_reader('user_categories')
read_author_link_map = dict_reader('author_link_map')
def dict_writer(name, serialize=dump_dict, remove2=True):
pq = '%s:%s' % (CALIBRE_PREFIX, name)
def writer(root, prefixes, refines, val):
if remove2:
for meta in XPath('./opf:metadata/opf:meta[@name="calibre:%s"]' % name)(root):
remove_element(meta, refines)
for meta in XPath('./opf:metadata/opf:meta[@property]')(root):
prop = expand_prefix(meta.get('property'), prefixes)
if prop.lower() == pq:
remove_element(meta, refines)
if val:
ensure_prefix(root, prefixes, 'calibre', CALIBRE_PREFIX)
m = XPath('./opf:metadata')(root)[0]
d = m.makeelement(OPF('meta'), attrib={'property':'calibre:%s' % name})
d.text = serialize(val)
m.append(d)
return writer
set_user_categories = dict_writer('user_categories')
set_author_link_map = dict_writer('author_link_map')
def deserialize_user_metadata(val):
val = json.loads(val, object_hook=from_json)
ans = {}
for name, fm in val.iteritems():
decode_is_multiple(fm)
ans[name] = fm
return ans
read_user_metadata3 = dict_reader('user_metadata', load=deserialize_user_metadata, try2=False)
def read_user_metadata2(root):
ans = {}
for meta in XPath('./opf:metadata/opf:meta[starts-with(@name, "calibre:user_metadata:")]')(root):
name = meta.get('name')
name = ':'.join(name.split(':')[2:])
if not name or not name.startswith('#'):
continue
fm = meta.get('content')
try:
fm = json.loads(fm, object_hook=from_json)
decode_is_multiple(fm)
ans[name] = fm
except Exception:
prints('Failed to read user metadata:', name)
import traceback
traceback.print_exc()
continue
return ans
def read_user_metadata(root, prefixes, refines):
return read_user_metadata3(root, prefixes, refines) or read_user_metadata2(root)
def serialize_user_metadata(val):
return json.dumps(object_to_unicode(val), ensure_ascii=False, default=to_json, indent=2, sort_keys=True)
set_user_metadata3 = dict_writer('user_metadata', serialize=serialize_user_metadata, remove2=False)
def set_user_metadata(root, prefixes, refines, val):
for meta in XPath('./opf:metadata/opf:meta[starts-with(@name, "calibre:user_metadata:")]')(root):
remove_element(meta, refines)
if val:
nval = {}
for name, fm in val.items():
fm = fm.copy()
encode_is_multiple(fm)
nval[name] = fm
set_user_metadata3(root, prefixes, refines, nval)
# }}}
def read_metadata(root): def read_metadata(root):
ans = Metadata(_('Unknown'), [_('Unknown')]) ans = Metadata(_('Unknown'), [_('Unknown')])
prefixes, refines = read_prefixes(root), read_refines(root) prefixes, refines = read_prefixes(root), read_refines(root)
@ -704,6 +813,10 @@ def read_metadata(root):
s, si = read_series(root, prefixes, refines) s, si = read_series(root, prefixes, refines)
if s: if s:
ans.series, ans.series_index = s, si ans.series, ans.series_index = s, si
ans.author_link_map = read_author_link_map(root, prefixes, refines) or ans.author_link_map
ans.user_categories = read_user_categories(root, prefixes, refines) or ans.user_categories
for name, fm in (read_user_metadata(root, prefixes, refines) or {}).iteritems():
ans.set_user_metadata(name, fm)
return ans return ans
def get_metadata(stream): def get_metadata(stream):
@ -727,6 +840,9 @@ def apply_metadata(root, mi, cover_prefix='', cover_data=None, apply_null=False,
set_tags(root, prefixes, refines, mi.tags) set_tags(root, prefixes, refines, mi.tags)
set_rating(root, prefixes, refines, mi.rating) set_rating(root, prefixes, refines, mi.rating)
set_series(root, prefixes, refines, mi.series, mi.series_index) set_series(root, prefixes, refines, mi.series, mi.series_index)
set_author_link_map(root, prefixes, refines, getattr(mi, 'author_link_map', None))
set_user_categories(root, prefixes, refines, getattr(mi, 'user_categories', None))
set_user_metadata(root, prefixes, refines, mi.get_all_user_metadata(False))
pretty_print_opf(root) pretty_print_opf(root)

View File

@ -17,8 +17,10 @@ from calibre.ebooks.metadata.opf3 import (
read_book_producers, set_book_producers, read_timestamp, set_timestamp, read_book_producers, set_book_producers, read_timestamp, set_timestamp,
read_pubdate, set_pubdate, CALIBRE_PREFIX, read_last_modified, read_comments, read_pubdate, set_pubdate, CALIBRE_PREFIX, read_last_modified, read_comments,
set_comments, read_publisher, set_publisher, read_tags, set_tags, read_rating, set_comments, read_publisher, set_publisher, read_tags, set_tags, read_rating,
set_rating, read_series, set_series set_rating, read_series, set_series, read_user_metadata, set_user_metadata,
read_author_link_map, read_user_categories, set_author_link_map, set_user_categories
) )
read_author_link_map, read_user_categories, set_author_link_map, set_user_categories
TEMPLATE = '''<package xmlns="http://www.idpf.org/2007/opf" version="3.0" prefix="calibre: %s" unique-identifier="uid"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">{metadata}</metadata></package>''' % CALIBRE_PREFIX # noqa TEMPLATE = '''<package xmlns="http://www.idpf.org/2007/opf" version="3.0" prefix="calibre: %s" unique-identifier="uid"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">{metadata}</metadata></package>''' % CALIBRE_PREFIX # noqa
default_refines = defaultdict(list) default_refines = defaultdict(list)
@ -235,6 +237,34 @@ class TestOPF3(unittest.TestCase):
self.ae(('zzz', 3.3), st(root, 'zzz', 3.3)) self.ae(('zzz', 3.3), st(root, 'zzz', 3.3))
# }}} # }}}
def test_user_metadata(self): # {{{
def rt(root, name):
f = globals()['read_' + name]
return f(root, read_prefixes(root), read_refines(root))
def st(root, name, val):
f = globals()['set_' + name]
f(root, read_prefixes(root), read_refines(root), val)
return rt(root, name)
for name in 'author_link_map user_categories'.split():
root = self.get_opf('''<meta name="calibre:%s" content='{"1":1}'/>''' % name)
self.ae({'1':1}, rt(root, name))
root = self.get_opf('''<meta name="calibre:%s" content='{"1":1}'/><meta property="calibre:%s">{"2":2}</meta>''' % (name, name))
self.ae({'2':2}, rt(root, name))
self.ae({'3':3}, st(root, name, {3:3}))
def ru(root):
return read_user_metadata(root, read_prefixes(root), read_refines(root))
def su(root, val):
set_user_metadata(root, read_prefixes(root), read_refines(root), val)
return ru(root)
root = self.get_opf('''<meta name="calibre:user_metadata:#a" content='{"1":1}'/>''')
self.ae({'#a': {'1': 1, 'is_multiple': {}}}, ru(root))
root = self.get_opf('''<meta name="calibre:user_metadata:#a" content='{"1":1}'/>'''
'''<meta property="calibre:user_metadata">{"#b":{"2":2}}</meta>''')
self.ae({'#b': {'2': 2, 'is_multiple': {}}}, ru(root))
self.ae({'#c': {'3': 3, 'is_multiple': {}, 'is_multiple2': {}}}, su(root, {'#c':{'3':3}}))
# }}}
# Run tests {{{ # Run tests {{{
def suite(): def suite():