From ebf08b4e22cb4ea450755be33a95009598fa8a29 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 14 Jun 2015 17:10:14 +0530 Subject: [PATCH] Implement /ajax/books(s) --- src/calibre/srv/ajax.py | 99 +++++++++++++++++++++++++++++++- src/calibre/srv/handler.py | 8 +++ src/calibre/srv/http_response.py | 1 + src/calibre/srv/tests/ajax.py | 45 +++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/calibre/srv/tests/ajax.py diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py index ff9d322960..6ac90054a8 100644 --- a/src/calibre/srv/ajax.py +++ b/src/calibre/srv/ajax.py @@ -11,9 +11,11 @@ from binascii import hexlify, unhexlify from calibre.ebooks.metadata import title_sort 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.utils import http_date from calibre.utils.config import prefs, tweaks -from calibre.utils.date import isoformat +from calibre.utils.date import isoformat, timestampfromdt def encode_name(name): if isinstance(name, unicode): @@ -23,6 +25,8 @@ def encode_name(name): def decode_name(name): return unhexlify(name).decode('utf-8') +# Book metadata {{{ + 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) @@ -101,6 +105,99 @@ def book_to_json(ctx, rd, db, book_id, 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) def books_in(ctx, rd, category, item): raise NotImplementedError('TODO: Implement this') diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 3ae7bb38ec..26e3c68980 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -72,6 +72,14 @@ class Context(object): def get_library(self, library_id=None): 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): def __init__(self, libraries, opts, testing=False): diff --git a/src/calibre/srv/http_response.py b/src/calibre/srv/http_response.py index 04e786a098..df113f6d5f 100644 --- a/src/calibre/srv/http_response.py +++ b/src/calibre/srv/http_response.py @@ -209,6 +209,7 @@ class RequestData(object): # {{{ self.lang_code = self.gettext_func = self.ngettext_func = None self.set_translator(self.get_preferred_language()) self.tdir = tdir + self.restrict_to_ids = {} def generate_static_output(self, name, generator): ans = self.static_cache.get(name) diff --git a/src/calibre/srv/tests/ajax.py b/src/calibre/srv/tests/ajax.py new file mode 100644 index 0000000000..99ce52c059 --- /dev/null +++ b/src/calibre/srv/tests/ajax.py @@ -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 ' + +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'}) + + # }}}