%PDF- %PDF-
Direktori : /lib/calibre/calibre/srv/ |
Current File : //lib/calibre/calibre/srv/code.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> import hashlib import random import shutil import sys import zipfile from json import load as load_json_file, loads as json_loads from threading import Lock from calibre import as_unicode from calibre.constants import in_develop_mode from calibre.customize.ui import available_input_formats from calibre.db.view import sanitize_sort_field_name from calibre.srv.ajax import search_result from calibre.srv.errors import ( BookNotFound, HTTPBadRequest, HTTPForbidden, HTTPNotFound ) from calibre.srv.metadata import ( book_as_json, categories_as_json, categories_settings, icon_map ) from calibre.srv.routes import endpoint, json from calibre.srv.utils import get_library_data, get_use_roman from calibre.utils.config import prefs, tweaks from calibre.utils.icu import numeric_sort_key, sort_key from calibre.utils.localization import ( get_lang, lang_map_for_ui, localize_website_link, lang_code_for_user_manual ) from calibre.utils.search_query_parser import ParseException from calibre.utils.serialize import json_dumps from polyglot.builtins import iteritems, itervalues POSTABLE = frozenset({'GET', 'POST', 'HEAD'}) @endpoint('', auth_required=False) def index(ctx, rd): ans_file = lopen(P('content-server/index-generated.html'), 'rb') if not in_develop_mode: return ans_file return ans_file.read().replace(b'__IN_DEVELOP_MODE__', b'1') @endpoint('/robots.txt', auth_required=False) def robots(ctx, rd): return b'User-agent: *\nDisallow: /' @endpoint('/ajax-setup', auth_required=False, cache_control='no-cache', postprocess=json) def ajax_setup(ctx, rd): auto_reload_port = getattr(rd.opts, 'auto_reload_port', 0) return { 'auto_reload_port': max(0, auto_reload_port), 'allow_console_print': bool(getattr(rd.opts, 'allow_console_print', False)), 'ajax_timeout': rd.opts.ajax_timeout, } print_lock = Lock() @endpoint('/console-print', methods=('POST', )) def console_print(ctx, rd): if not getattr(rd.opts, 'allow_console_print', False): raise HTTPForbidden('console printing is not allowed') with print_lock: print(rd.remote_addr, end=' ') stdout = getattr(sys.stdout, 'buffer', sys.stdout) shutil.copyfileobj(rd.request_body_file, stdout) stdout.flush() return '' def get_basic_query_data(ctx, rd): db, library_id, library_map, default_library = get_library_data(ctx, rd) skeys = db.field_metadata.sortable_field_keys() sorts, orders = [], [] for x in rd.query.get('sort', '').split(','): if x: s, o = x.rpartition('.')[::2] if o and not s: s, o = o, '' if o not in ('asc', 'desc'): o = 'asc' if s.startswith('_'): s = '#' + s[1:] s = sanitize_sort_field_name(db.field_metadata, s) if s in skeys: sorts.append(s), orders.append(o) if not sorts: sorts, orders = ['timestamp'], ['desc'] return library_id, db, sorts, orders, rd.query.get('vl') or '' def get_translations_data(): with zipfile.ZipFile( P('content-server/locales.zip', allow_user_override=False), 'r' ) as zf: names = set(zf.namelist()) lang = get_lang() if lang not in names: xlang = lang.split('_')[0].lower() if xlang in names: lang = xlang if lang in names: return zf.open(lang, 'r').read() def get_translations(): if not hasattr(get_translations, 'cached'): get_translations.cached = False data = get_translations_data() if data: get_translations.cached = json_loads(data) return get_translations.cached def custom_list_template(): ans = getattr(custom_list_template, 'ans', None) if ans is None: ans = { 'thumbnail': True, 'thumbnail_height': 140, 'height': 'auto', 'comments_fields': ['comments'], 'lines': [ _('<b>{title}</b> by {authors}'), _('{series_index} of <i>{series}</i>') + '|||{rating}', '{tags}', _('Date: {timestamp}') + '|||' + _('Published: {pubdate}') + '|||' + _('Publisher: {publisher}'), '', ] } custom_list_template.ans = ans return ans def basic_interface_data(ctx, rd): ans = { 'username': rd.username, 'output_format': prefs['output_format'].upper(), 'input_formats': {x.upper(): True for x in available_input_formats()}, 'gui_pubdate_display_format': tweaks['gui_pubdate_display_format'], 'gui_timestamp_display_format': tweaks['gui_timestamp_display_format'], 'gui_last_modified_display_format': tweaks['gui_last_modified_display_format'], 'completion_mode': tweaks['completion_mode'], 'use_roman_numerals_for_series_number': get_use_roman(), 'translations': get_translations(), 'icon_map': icon_map(), 'icon_path': ctx.url_for('/icon', which=''), 'custom_list_template': getattr(ctx, 'custom_list_template', None) or custom_list_template(), 'search_the_net_urls': getattr(ctx, 'search_the_net_urls', None) or [], 'num_per_page': rd.opts.num_per_page, 'default_book_list_mode': rd.opts.book_list_mode, 'donate_link': localize_website_link('https://calibre-ebook.com/donate'), 'lang_code_for_user_manual': lang_code_for_user_manual(), } ans['library_map'], ans['default_library_id'] = ctx.library_info(rd) return ans @endpoint('/interface-data/update/{translations_hash=None}', postprocess=json) def update_interface_data(ctx, rd, translations_hash): ''' Return the interface data needed for the server UI ''' ans = basic_interface_data(ctx, rd) t = ans['translations'] if t and (t.get('hash') or translations_hash) and t.get('hash') == translations_hash: del ans['translations'] return ans def get_field_list(db): fieldlist = list(db.pref('book_display_fields', ())) names = frozenset(x[0] for x in fieldlist) available = frozenset(db.field_metadata.displayable_field_keys()) for field in available: if field not in names: fieldlist.append((field, True)) return [f for f, d in fieldlist if d and f in available] def get_library_init_data(ctx, rd, db, num, sorts, orders, vl): ans = {} with db.safe_read_lock: try: ans['search_result'] = search_result( ctx, rd, db, rd.query.get('search', ''), num, 0, ','.join(sorts), ','.join(orders), vl ) except ParseException: ans['search_result'] = search_result( ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders), vl ) sf = db.field_metadata.ui_sortable_field_keys() sf.pop('ondevice', None) ans['sortable_fields'] = sorted( ((sanitize_sort_field_name(db.field_metadata, k), v) for k, v in iteritems(sf)), key=lambda field_name: sort_key(field_name[1]) ) ans['field_metadata'] = db.field_metadata.all_metadata() ans['virtual_libraries'] = db._pref('virtual_libraries', {}) ans['bools_are_tristate'] = db._pref('bools_are_tristate', True) ans['book_display_fields'] = get_field_list(db) mdata = ans['metadata'] = {} try: extra_books = { int(x) for x in rd.query.get('extra_books', '').split(',') } except Exception: extra_books = () for coll in (ans['search_result']['book_ids'], extra_books): for book_id in coll: if book_id not in mdata: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans @endpoint('/interface-data/books-init', postprocess=json) def books(ctx, rd): ''' Get data to create list of books Optional: ?num=50&sort=timestamp.desc&library_id=<default library> &search=''&extra_books=''&vl='' ''' ans = {} try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) library_id, db, sorts, orders, vl = get_basic_query_data(ctx, rd) ans = get_library_init_data(ctx, rd, db, num, sorts, orders, vl) ans['library_id'] = library_id return ans @endpoint('/interface-data/init', postprocess=json) def interface_data(ctx, rd): ''' Return the data needed to create the server UI as well as a list of books. Optional: ?num=50&sort=timestamp.desc&library_id=<default library> &search=''&extra_books=''&vl='' ''' ans = basic_interface_data(ctx, rd) ud = {} if rd.username: # Override session data with stored values for the authenticated user, # if any ud = ctx.user_manager.get_session_data(rd.username) lid = ud.get('library_id') if lid and lid in ans['library_map']: rd.query.set('library_id', lid) usort = ud.get('sort') if usort: rd.query.set('sort', usort) ans['library_id'], db, sorts, orders, vl = get_basic_query_data(ctx, rd) ans['user_session_data'] = ud try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) ans.update(get_library_init_data(ctx, rd, db, num, sorts, orders, vl)) return ans @endpoint('/interface-data/more-books', postprocess=json, methods=POSTABLE) def more_books(ctx, rd): ''' Get more results from the specified search-query, which must be specified as JSON in the request body. Optional: ?num=50&library_id=<default library> ''' db, library_id = get_library_data(ctx, rd)[:2] try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) try: search_query = load_json_file(rd.request_body_file) query, offset, sorts, orders, vl = search_query['query'], search_query[ 'offset' ], search_query['sort'], search_query['sort_order'], search_query['vl'] except KeyError as err: raise HTTPBadRequest('Search query missing key: %s' % as_unicode(err)) except Exception as err: raise HTTPBadRequest('Invalid query: %s' % as_unicode(err)) ans = {} with db.safe_read_lock: ans['search_result'] = search_result( ctx, rd, db, query, num, offset, sorts, orders, vl ) mdata = ans['metadata'] = {} for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans @endpoint('/interface-data/set-session-data', postprocess=json, methods=POSTABLE) def set_session_data(ctx, rd): ''' Store session data persistently so that it is propagated automatically to new logged in clients ''' if rd.username: try: new_data = load_json_file(rd.request_body_file) if not isinstance(new_data, dict): raise Exception('session data must be a dict') except Exception as err: raise HTTPBadRequest('Invalid data: %s' % as_unicode(err)) ud = ctx.user_manager.get_session_data(rd.username) ud.update(new_data) ctx.user_manager.set_session_data(rd.username, ud) @endpoint('/interface-data/get-books', postprocess=json) def get_books(ctx, rd): ''' Get books for the specified query Optional: ?library_id=<default library>&num=50&sort=timestamp.desc&search=''&vl='' ''' library_id, db, sorts, orders, vl = get_basic_query_data(ctx, rd) try: num = int(rd.query.get('num', rd.opts.num_per_page)) except Exception: raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num')) searchq = rd.query.get('search', '') db = get_library_data(ctx, rd)[0] ans = {} mdata = ans['metadata'] = {} with db.safe_read_lock: try: ans['search_result'] = search_result( ctx, rd, db, searchq, num, 0, ','.join(sorts), ','.join(orders), vl ) except ParseException as err: # This must not be translated as it is used by the front end to # detect invalid search expressions raise HTTPBadRequest('Invalid search expression: %s' % as_unicode(err)) for book_id in ans['search_result']['book_ids']: data = book_as_json(db, book_id) if data is not None: mdata[book_id] = data return ans @endpoint('/interface-data/book-metadata/{book_id=0}', postprocess=json) def book_metadata(ctx, rd, book_id): ''' Get metadata for the specified book. If no book_id is specified, return metadata for a random book. Optional: ?library_id=<default library>&vl=<virtual library> ''' library_id, db, sorts, orders, vl = get_basic_query_data(ctx, rd) if not book_id: all_ids = ctx.allowed_book_ids(rd, db) book_id = random.choice(tuple(all_ids)) elif not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) data = book_as_json(db, book_id) if data is None: raise BookNotFound(book_id, db) data['id'] = book_id # needed for random book view (when book_id=0) return data @endpoint('/interface-data/tag-browser') def tag_browser(ctx, rd): ''' Get the Tag Browser serialized as JSON Optional: ?library_id=<default library>&sort_tags_by=name&partition_method=first letter &collapse_at=25&dont_collapse=&hide_empty_categories=&vl='' ''' db, library_id = get_library_data(ctx, rd)[:2] opts = categories_settings(rd.query, db, gst_container=tuple) vl = rd.query.get('vl') or '' etag = json_dumps([db.last_modified().isoformat(), rd.username, library_id, vl, list(opts)]) etag = hashlib.sha1(etag).hexdigest() def generate(): return json(ctx, rd, tag_browser, categories_as_json(ctx, rd, db, opts, vl)) return rd.etagged_dynamic_response(etag, generate) def all_lang_names(): ans = getattr(all_lang_names, 'ans', None) if ans is None: ans = all_lang_names.ans = tuple(sorted(itervalues(lang_map_for_ui()), key=numeric_sort_key)) return ans @endpoint('/interface-data/field-names/{field}', postprocess=json) def field_names(ctx, rd, field): ''' Get a list of all names for the specified field Optional: ?library_id=<default library> ''' if field == 'languages': ans = all_lang_names() else: db, library_id = get_library_data(ctx, rd)[:2] ans = tuple(sorted(db.all_field_names(field), key=numeric_sort_key)) return ans