diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index f1bcc4b220..5a88f79f46 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -237,6 +237,17 @@ class ReadingTest(BaseTest): self.compare_metadata(mi1, mi2) # }}} + def test_serialize_metadata(self): # {{{ + from calibre.utils.serialize import json_dumps, json_loads, msgpack_dumps, msgpack_loads + cache = self.init_cache(self.library_path) + for i in xrange(1, 4): + mi = cache.get_metadata(i, get_cover=True, cover_as_data=True) + rmi = msgpack_loads(msgpack_dumps(mi)) + self.compare_metadata(mi, rmi, exclude='format_metadata has_cover formats id'.split()) + rmi = json_loads(json_dumps(mi)) + self.compare_metadata(mi, rmi, exclude='format_metadata has_cover formats id'.split()) + # }}} + def test_get_cover(self): # {{{ 'Test cover() returns the same data for both backends' from calibre.library.database2 import LibraryDatabase2 diff --git a/src/calibre/ebooks/metadata/book/serialize.py b/src/calibre/ebooks/metadata/book/serialize.py new file mode 100644 index 0000000000..8f7f46fa4a --- /dev/null +++ b/src/calibre/ebooks/metadata/book/serialize.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2017, Kovid Goyal + +from __future__ import absolute_import, division, print_function, unicode_literals + +import base64 + +from calibre.constants import preferred_encoding +from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS +from calibre.ebooks.metadata.book.base import Metadata +from calibre.utils.imghdr import what + + +def ensure_unicode(obj, enc=preferred_encoding): + if isinstance(obj, unicode): + return obj + if isinstance(obj, bytes): + return obj.decode(enc, 'replace') + if isinstance(obj, (list, tuple)): + return [ensure_unicode(x) for x in obj] + if isinstance(obj, dict): + return {ensure_unicode(k): ensure_unicode(v) for k, v in obj.iteritems()} + return obj + + +def read_cover(mi): + if mi.cover_data and mi.cover_data[1]: + return + if mi.cover: + try: + with lopen(mi.cover, 'rb') as f: + cd = f.read() + mi.cover_data = what(cd), cd + except EnvironmentError: + pass + return mi + + +def metadata_as_dict(mi, encode_cover_data=False): + if hasattr(mi, 'to_book_metadata'): + mi = mi.to_book_metadata() + ans = {} + for field in SERIALIZABLE_FIELDS: + if field != 'cover' and not mi.is_null(field): + val = getattr(mi, field) + ans[field] = ensure_unicode(val) + if mi.cover_data and mi.cover_data[1]: + if encode_cover_data: + ans['cover_data'] = [mi.cover_data[0], base64.standard_b64encode(bytes(mi.cover_data[1]))] + else: + ans['cover_data'] = mi.cover_data + um = mi.get_all_user_metadata(False) + if um: + ans['user_metadata'] = um + return ans + + +def metadata_from_dict(src): + ans = Metadata('Unknown') + for key, value in src.iteritems(): + if key == 'user_metadata': + ans.set_all_user_metadata(value) + else: + setattr(ans, key, value) + return ans diff --git a/src/calibre/utils/serialize.py b/src/calibre/utils/serialize.py index 7921d614d8..b71762ce1d 100644 --- a/src/calibre/utils/serialize.py +++ b/src/calibre/utils/serialize.py @@ -4,17 +4,23 @@ from __future__ import absolute_import, division, print_function, unicode_literals -import json +import json, base64 +from functools import partial from datetime import datetime -from calibre.utils.iso8601 import parse_iso8601 +from calibre.utils.iso8601 import parse_iso8601 MSGPACK_MIME = 'application/x-msgpack' -def encoder(obj): +def encoder(obj, for_json=False): if isinstance(obj, datetime): - return {'__datetime__': obj.isoformat()} + return {'__datetime__': unicode(obj.isoformat())} + if obj.__class__.__name__ == 'Metadata': + from calibre.ebooks.metadata.book.base import Metadata + if isinstance(obj, Metadata): + from calibre.ebooks.metadata.book.serialize import metadata_as_dict + obj = {'__metadata__': metadata_as_dict(obj, encode_cover_data=for_json)} return obj @@ -24,7 +30,7 @@ def msgpack_dumps(data): def json_dumps(data, **kw): - kw['default'] = encoder + kw['default'] = partial(encoder, for_json=True) kw['ensure_ascii'] = False ans = json.dumps(data, **kw) if not isinstance(ans, bytes): @@ -32,19 +38,24 @@ def json_dumps(data, **kw): return ans -def decoder(obj): +def decoder(obj, for_json=False): dt = obj.get('__datetime__') if dt is not None: - obj = parse_iso8601(dt, assume_utc=True) + return parse_iso8601(dt, assume_utc=True) + m = obj.get('__metadata__') + if m is not None: + from calibre.ebooks.metadata.book.serialize import metadata_from_dict + obj = metadata_from_dict(m) + if for_json and obj.cover_data and obj.cover_data[1]: + obj.cover_data = obj.cover_data[0], base64.standard_b64decode(obj.cover_data[1]) + return obj return obj def msgpack_loads(data): import msgpack - return msgpack.unpackb( - data, encoding='utf-8', use_list=False, object_hook=decoder - ) + return msgpack.unpackb(data, encoding='utf-8', object_hook=decoder) def json_loads(data): - return json.loads(data, object_hook=decoder) + return json.loads(data, object_hook=partial(decoder, for_json=True))