Implement /ajax/books(s)

This commit is contained in:
Kovid Goyal 2015-06-14 17:10:14 +05:30
parent 7fc14ca47f
commit ebf08b4e22
4 changed files with 152 additions and 1 deletions

View File

@ -11,9 +11,11 @@ from binascii import hexlify, unhexlify
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.srv.errors import HTTPNotFound
from calibre.srv.routes import endpoint, json from calibre.srv.routes import endpoint, json
from calibre.srv.utils import http_date
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import isoformat from calibre.utils.date import isoformat, timestampfromdt
def encode_name(name): def encode_name(name):
if isinstance(name, unicode): if isinstance(name, unicode):
@ -23,6 +25,8 @@ def encode_name(name):
def decode_name(name): def decode_name(name):
return unhexlify(name).decode('utf-8') return unhexlify(name).decode('utf-8')
# Book metadata {{{
def book_to_json(ctx, rd, db, book_id, def book_to_json(ctx, rd, db, book_id,
get_category_urls=True, device_compatible=False, device_for_template=None): get_category_urls=True, device_compatible=False, device_for_template=None):
mi = db.get_metadata(book_id, get_cover=False) mi = db.get_metadata(book_id, get_cover=False)
@ -101,6 +105,99 @@ def book_to_json(ctx, rd, db, book_id,
return data, mi.last_modified return data, mi.last_modified
@endpoint('/ajax/book/{book_id}/{library_id=None}', postprocess=json)
def book(ctx, rd, book_id, library_id):
'''
Return the metadata of the book as a JSON dictionary.
Query parameters: ?category_urls=true&id_is_uuid=false&device_for_template=None
If category_urls is true the returned dictionary also contains a
mapping of category (field) names to URLs that return the list of books in the
given category.
If id_is_uuid is true then the book_id is assumed to be a book uuid instead.
'''
db = ctx.get_library(library_id)
if db is None:
raise HTTPNotFound('Library %r not found' % library_id)
with db.safe_read_lock:
id_is_uuid = rd.query.get('id_is_uuid', 'false')
oid = book_id
if id_is_uuid == 'true':
book_id = db.lookup_by_uuid(book_id)
else:
try:
book_id = int(book_id)
if not db.has_id(book_id):
book_id = None
except Exception:
book_id = None
if book_id is None:
raise HTTPNotFound('Book with id %r does not exist' % oid)
category_urls = rd.query.get('category_urls', 'true').lower()
device_compatible = rd.query.get('device_compatible', 'false').lower()
device_for_template = rd.query.get('device_for_template', None)
data, last_modified = book_to_json(ctx, rd, db, book_id,
get_category_urls=category_urls == 'true',
device_compatible=device_compatible == 'true',
device_for_template=device_for_template)
rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified))
return data
@endpoint('/ajax/books/{library_id=None}', postprocess=json)
def books(ctx, rd, library_id):
'''
Return the metadata for the books as a JSON dictionary.
Query parameters: ?ids=all&category_urls=true&id_is_uuid=false&device_for_template=None
If category_urls is true the returned dictionary also contains a
mapping of category (field) names to URLs that return the list of books in the
given category.
If id_is_uuid is true then the book_id is assumed to be a book uuid instead.
'''
db = ctx.get_library(library_id)
if db is None:
raise HTTPNotFound('Library %r not found' % library_id)
with db.safe_read_lock:
id_is_uuid = rd.query.get('id_is_uuid', 'false')
ids = rd.query.get('ids')
if ids is None or ids == 'all':
ids = db.all_book_ids()
else:
ids = ids.split(',')
if id_is_uuid == 'true':
ids = {db.lookup_by_uuid(x) for x in ids}
ids.discard(None)
else:
try:
ids = {int(x) for x in ids}
except Exception:
raise HTTPNotFound('ids must a comma separated list of integers')
last_modified = None
category_urls = rd.query.get('category_urls', 'true').lower() == 'true'
device_compatible = rd.query.get('device_compatible', 'false').lower() == 'true'
device_for_template = rd.query.get('device_for_template', None)
ans = {}
restricted_to = ctx.restrict_to_ids(db, rd)
for book_id in ids:
if book_id not in restricted_to:
ans[book_id] = None
continue
data, lm = book_to_json(
ctx, rd, db, book_id, get_category_urls=category_urls,
device_compatible=device_compatible, device_for_template=device_for_template)
last_modified = lm if last_modified is None else max(lm, last_modified)
ans[book_id] = data
if last_modified is not None:
rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified))
return ans
# }}}
@endpoint('/ajax/books_in/{category}/{item}', postprocess=json) @endpoint('/ajax/books_in/{category}/{item}', postprocess=json)
def books_in(ctx, rd, category, item): def books_in(ctx, rd, category, item):
raise NotImplementedError('TODO: Implement this') raise NotImplementedError('TODO: Implement this')

View File

@ -72,6 +72,14 @@ class Context(object):
def get_library(self, library_id=None): def get_library(self, library_id=None):
return self.library_broker.get(library_id) return self.library_broker.get(library_id)
def restrict_to_ids(self, db, data):
# TODO: Implement this based on data.username caching result on the
# data object
ans = data.restrict_to_ids.get(db.server_library_id)
if ans is None:
ans = data.restrict_to_ids[db.server_library_id] = db.all_book_ids()
return ans
class Handler(object): class Handler(object):
def __init__(self, libraries, opts, testing=False): def __init__(self, libraries, opts, testing=False):

View File

@ -209,6 +209,7 @@ class RequestData(object): # {{{
self.lang_code = self.gettext_func = self.ngettext_func = None self.lang_code = self.gettext_func = self.ngettext_func = None
self.set_translator(self.get_preferred_language()) self.set_translator(self.get_preferred_language())
self.tdir = tdir self.tdir = tdir
self.restrict_to_ids = {}
def generate_static_output(self, name, generator): def generate_static_output(self, name, generator):
ans = self.static_cache.get(name) ans = self.static_cache.get(name)

View File

@ -0,0 +1,45 @@
#!/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>'
import httplib, zlib, json
from calibre.srv.tests.base import LibraryBaseTest
class ContentTest(LibraryBaseTest):
def test_ajax_book(self): # {{{
'Test /ajax/book'
with self.create_server() as server:
db = server.handler.router.ctx.get_library()
conn = server.connect()
def request(url, headers={}):
conn.request('GET', '/ajax/book' + url, headers=headers)
r = conn.getresponse()
data = r.read()
if r.status == httplib.OK and data.startswith(b'{'):
data = json.loads(data)
return r, data
r, data = request('/x')
self.ae(r.status, httplib.NOT_FOUND)
r, onedata = request('/1')
self.ae(r.status, httplib.OK)
self.ae(request('/1/' + db.server_library_id)[1], onedata)
self.ae(request('/%s?id_is_uuid=true' % db.field_for('uuid', 1))[1], onedata)
r, data = request('s')
self.ae(set(data.iterkeys()), set(map(str, db.all_book_ids())))
r, zdata = request('s', headers={'Accept-Encoding':'gzip'})
self.ae(r.getheader('Content-Encoding'), 'gzip')
self.ae(json.loads(zlib.decompress(zdata, 16+zlib.MAX_WBITS)), data)
r, data = request('s?ids=1,2')
self.ae(set(data.iterkeys()), {'1', '2'})
# }}}