From 3d2066c5c2e7648e3f5d3b8e079bd674fb1a4c6a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 14 Jun 2015 14:01:36 +0530 Subject: [PATCH] Implement /get/json --- .../ebooks/metadata/book/json_codec.py | 4 +- src/calibre/srv/ajax.py | 106 ++++++++++++++++++ src/calibre/srv/content.py | 7 +- src/calibre/srv/handler.py | 2 +- src/calibre/srv/routes.py | 18 ++- src/calibre/srv/tests/content.py | 12 +- 6 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 src/calibre/srv/ajax.py diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py index 8c1fdc7b33..8cbc7b70e2 100644 --- a/src/calibre/ebooks/metadata/book/json_codec.py +++ b/src/calibre/ebooks/metadata/book/json_codec.py @@ -118,8 +118,8 @@ def decode_is_multiple(fm): class JsonCodec(object): - def __init__(self): - self.field_metadata = FieldMetadata() + def __init__(self, field_metadata=None): + self.field_metadata = field_metadata or FieldMetadata() def encode_to_file(self, file_, booklist): file_.write(json.dumps(self.encode_booklist_metadata(booklist), diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py new file mode 100644 index 0000000000..ff9d322960 --- /dev/null +++ b/src/calibre/srv/ajax.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Kovid Goyal ' + +from functools import partial +from binascii import hexlify, unhexlify + +from calibre.ebooks.metadata import title_sort +from calibre.ebooks.metadata.book.json_codec import JsonCodec +from calibre.srv.routes import endpoint, json +from calibre.utils.config import prefs, tweaks +from calibre.utils.date import isoformat + +def encode_name(name): + if isinstance(name, unicode): + name = name.encode('utf-8') + return hexlify(name) + +def decode_name(name): + return unhexlify(name).decode('utf-8') + +def book_to_json(ctx, rd, db, book_id, + get_category_urls=True, device_compatible=False, device_for_template=None): + mi = db.get_metadata(book_id, get_cover=False) + codec = JsonCodec(db.field_metadata) + if not device_compatible: + try: + mi.rating = mi.rating/2. + except Exception: + mi.rating = 0.0 + data = codec.encode_book_metadata(mi) + for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime', + 'rights', 'book_producer'): + data.pop(x, None) + + get = partial(ctx.url_for, '/get', book_id=book_id, library_id=db.server_library_id) + data['cover'] = get(what='cover') + data['thumbnail'] = get(what='thumb') + + if not device_compatible: + mi.format_metadata = {k.lower():dict(v) for k, v in + mi.format_metadata.iteritems()} + for v in mi.format_metadata.itervalues(): + mtime = v.get('mtime', None) + if mtime is not None: + v['mtime'] = isoformat(mtime, as_utc=True) + data['format_metadata'] = mi.format_metadata + fmts = set(x.lower() for x in mi.format_metadata.iterkeys()) + pf = prefs['output_format'].lower() + other_fmts = list(fmts) + try: + fmt = pf if pf in fmts else other_fmts[0] + except: + fmt = None + if fmts and fmt: + other_fmts = [x for x in fmts if x != fmt] + data['formats'] = sorted(fmts) + if fmt: + data['main_format'] = {fmt:get(what=fmt)} + else: + data['main_format'] = None + data['other_formats'] = {fmt:get(what=fmt) for fmt in other_fmts} + + if get_category_urls: + category_urls = data['category_urls'] = {} + for key in mi.all_field_keys(): + fm = mi.metadata_for_field(key) + if (fm and fm['is_category'] and not fm['is_csp'] and + key != 'formats' and fm['datatype'] != 'rating'): + categories = mi.get(key) or [] + if isinstance(categories, basestring): + categories = [categories] + idmap = db.get_item_ids(key, categories) + category_urls[key] = dbtags = {} + for category, category_id in idmap.iteritems(): + if category_id is not None: + dbtags[category] = ctx.url_for( + '/ajax/books_in', category=encode_name(key), item=encode_name(str(category_id))) + else: + series = data.get('series', None) or '' + if series: + tsorder = tweaks['save_template_title_series_sorting'] + series = title_sort(series, order=tsorder) + data['_series_sort_'] = series + if device_for_template: + import posixpath + from calibre.devices.utils import create_upload_path + from calibre.utils.filenames import ascii_filename as sanitize + from calibre.customize.ui import device_plugins + + for device_class in device_plugins(): + if device_class.__class__.__name__ == device_for_template: + template = device_class.save_template() + data['_filename_'] = create_upload_path(mi, book_id, + template, sanitize, path_type=posixpath) + break + + return data, mi.last_modified + +@endpoint('/ajax/books_in/{category}/{item}', postprocess=json) +def books_in(ctx, rd, category, item): + raise NotImplementedError('TODO: Implement this') diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py index d2d46d1769..fea7f0eaac 100644 --- a/src/calibre/srv/content.py +++ b/src/calibre/srv/content.py @@ -14,8 +14,9 @@ from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.library.save_to_disk import find_plugboard +from calibre.srv.ajax import book_to_json from calibre.srv.errors import HTTPNotFound -from calibre.srv.routes import endpoint +from calibre.srv.routes import endpoint, json from calibre.srv.utils import http_date from calibre.utils.config_base import tweaks from calibre.utils.date import timestampfromdt @@ -162,7 +163,9 @@ def get(ctx, rd, what, book_id, library_id): rd.outheaders['Last-Modified'] = http_date(timestampfromdt(mi.last_modified)) return metadata_to_opf(mi) elif what == 'json': - raise NotImplementedError('TODO: Implement this') + data, last_modified = book_to_json(ctx, rd, db, book_id) + rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified)) + return json(ctx, rd, get, data) else: try: return book_fmt(ctx, rd, library_id, db, book_id, what.lower()) diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 379e63d26c..3ae7bb38ec 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -76,7 +76,7 @@ class Handler(object): def __init__(self, libraries, opts, testing=False): self.router = Router(ctx=Context(libraries, opts, testing=testing), url_prefix=opts.url_prefix) - for module in ('content',): + for module in ('content', 'ajax'): module = import_module('calibre.srv.' + module) self.router.load_routes(vars(module).itervalues()) self.router.finalize() diff --git a/src/calibre/srv/routes.py b/src/calibre/srv/routes.py index 413ca4ef95..6fcd767e6f 100644 --- a/src/calibre/srv/routes.py +++ b/src/calibre/srv/routes.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' -import httplib, sys, inspect, re, time, numbers +import httplib, sys, inspect, re, time, numbers, json as jsonlib from itertools import izip from operator import attrgetter @@ -15,6 +15,13 @@ from calibre.srv.utils import http_date default_methods = frozenset(('HEAD', 'GET')) +def json(ctx, rd, endpoint, output): + rd.outheaders['Content-Type'] = 'application/json; charset=UTF-8' + ans = jsonlib.dumps(output, ensure_ascii=False) + if not isinstance(ans, bytes): + ans = ans.encode('utf-8') + return ans + def route_key(route): return route.partition('{')[0].rstrip('/') @@ -29,7 +36,9 @@ def endpoint(route, # Set to a number to cache for at most number hours # Set to a tuple (cache_type, max_age) to explicitly set the # Cache-Control header - cache_control=False + cache_control=False, + + postprocess=None ): def annotate(f): f.route = route.rstrip('/') or '/' @@ -38,6 +47,7 @@ def endpoint(route, f.auth_required = auth_required f.android_workaround = android_workaround f.cache_control = cache_control + f.postprocess = postprocess f.is_endpoint = True return f return annotate @@ -223,6 +233,10 @@ class Router(object): self.finalize_session(endpoint_, data, ans) outheaders = data.outheaders + pp = endpoint_.postprocess + if pp is not None: + ans = pp(self.ctx, data, endpoint_, ans) + cc = endpoint_.cache_control if cc is not False and 'Cache-Control' not in data.outheaders: if cc is None or cc == 'no-cache': diff --git a/src/calibre/srv/tests/content.py b/src/calibre/srv/tests/content.py index 44c205dc06..b6a54840ef 100644 --- a/src/calibre/srv/tests/content.py +++ b/src/calibre/srv/tests/content.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' -import httplib, zlib +import httplib, zlib, json from io import BytesIO from calibre.ebooks.metadata.epub import get_metadata @@ -164,4 +164,14 @@ class ContentTest(LibraryBaseTest): raw = r.read() self.ae(zlib.decompress(raw, 16+zlib.MAX_WBITS), data) + # Test serving metadata as json + r, data = get('json', 1) + self.ae(r.status, httplib.OK) + self.ae(db.field_for('title', 1), json.loads(data)['title']) + conn.request('GET', '/get/json/1', headers={'Accept-Encoding':'gzip'}) + r = conn.getresponse() + self.ae(r.status, httplib.OK), self.ae(r.getheader('Content-Encoding'), 'gzip') + raw = r.read() + self.ae(zlib.decompress(raw, 16+zlib.MAX_WBITS), data) + # }}}