Implement /get/json

This commit is contained in:
Kovid Goyal 2015-06-14 14:01:36 +05:30
parent 5ef9def403
commit 3d2066c5c2
6 changed files with 141 additions and 8 deletions

View File

@ -118,8 +118,8 @@ def decode_is_multiple(fm):
class JsonCodec(object): class JsonCodec(object):
def __init__(self): def __init__(self, field_metadata=None):
self.field_metadata = FieldMetadata() self.field_metadata = field_metadata or FieldMetadata()
def encode_to_file(self, file_, booklist): def encode_to_file(self, file_, booklist):
file_.write(json.dumps(self.encode_booklist_metadata(booklist), file_.write(json.dumps(self.encode_booklist_metadata(booklist),

106
src/calibre/srv/ajax.py Normal file
View File

@ -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 <kovid at kovidgoyal.net>'
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')

View File

@ -14,8 +14,9 @@ from calibre.ebooks.metadata import authors_to_string
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.library.save_to_disk import find_plugboard 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.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.srv.utils import http_date
from calibre.utils.config_base import tweaks from calibre.utils.config_base import tweaks
from calibre.utils.date import timestampfromdt 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)) rd.outheaders['Last-Modified'] = http_date(timestampfromdt(mi.last_modified))
return metadata_to_opf(mi) return metadata_to_opf(mi)
elif what == 'json': 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: else:
try: try:
return book_fmt(ctx, rd, library_id, db, book_id, what.lower()) return book_fmt(ctx, rd, library_id, db, book_id, what.lower())

View File

@ -76,7 +76,7 @@ class Handler(object):
def __init__(self, libraries, opts, testing=False): def __init__(self, libraries, opts, testing=False):
self.router = Router(ctx=Context(libraries, opts, testing=testing), url_prefix=opts.url_prefix) 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) module = import_module('calibre.srv.' + module)
self.router.load_routes(vars(module).itervalues()) self.router.load_routes(vars(module).itervalues())
self.router.finalize() self.router.finalize()

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import httplib, sys, inspect, re, time, numbers import httplib, sys, inspect, re, time, numbers, json as jsonlib
from itertools import izip from itertools import izip
from operator import attrgetter from operator import attrgetter
@ -15,6 +15,13 @@ from calibre.srv.utils import http_date
default_methods = frozenset(('HEAD', 'GET')) 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): def route_key(route):
return route.partition('{')[0].rstrip('/') 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 number to cache for at most number hours
# Set to a tuple (cache_type, max_age) to explicitly set the # Set to a tuple (cache_type, max_age) to explicitly set the
# Cache-Control header # Cache-Control header
cache_control=False cache_control=False,
postprocess=None
): ):
def annotate(f): def annotate(f):
f.route = route.rstrip('/') or '/' f.route = route.rstrip('/') or '/'
@ -38,6 +47,7 @@ def endpoint(route,
f.auth_required = auth_required f.auth_required = auth_required
f.android_workaround = android_workaround f.android_workaround = android_workaround
f.cache_control = cache_control f.cache_control = cache_control
f.postprocess = postprocess
f.is_endpoint = True f.is_endpoint = True
return f return f
return annotate return annotate
@ -223,6 +233,10 @@ class Router(object):
self.finalize_session(endpoint_, data, ans) self.finalize_session(endpoint_, data, ans)
outheaders = data.outheaders outheaders = data.outheaders
pp = endpoint_.postprocess
if pp is not None:
ans = pp(self.ctx, data, endpoint_, ans)
cc = endpoint_.cache_control cc = endpoint_.cache_control
if cc is not False and 'Cache-Control' not in data.outheaders: if cc is not False and 'Cache-Control' not in data.outheaders:
if cc is None or cc == 'no-cache': if cc is None or cc == 'no-cache':

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import httplib, zlib import httplib, zlib, json
from io import BytesIO from io import BytesIO
from calibre.ebooks.metadata.epub import get_metadata from calibre.ebooks.metadata.epub import get_metadata
@ -164,4 +164,14 @@ class ContentTest(LibraryBaseTest):
raw = r.read() raw = r.read()
self.ae(zlib.decompress(raw, 16+zlib.MAX_WBITS), data) 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)
# }}} # }}}