diff --git a/resources/content-server/index.html b/resources/content-server/index.html
index 5460c8cc33..f93c9522db 100644
--- a/resources/content-server/index.html
+++ b/resources/content-server/index.html
@@ -6,7 +6,7 @@
-
+
diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py
index 95bb66d488..701899206c 100644
--- a/src/calibre/srv/ajax.py
+++ b/src/calibre/srv/ajax.py
@@ -18,7 +18,6 @@ from calibre.ebooks.metadata.book.json_codec import JsonCodec
from calibre.srv.errors import HTTPNotFound
from calibre.srv.metadata import book_as_json
from calibre.srv.routes import endpoint, json
-from calibre.srv.session import defaults
from calibre.srv.content import get as get_content, icon as get_icon
from calibre.srv.utils import http_date, custom_fields_to_display, encode_name, decode_name
from calibre.utils.config import prefs, tweaks
@@ -562,27 +561,43 @@ def search(ctx, rd, library_id):
return _search(ctx, rd, db, query, num, offset, rd.query.get('sort', 'title'), rd.query.get('sort_order', 'asc'))
# }}}
+def get_basic_query_data(ctx, query):
+ library_id = query.get('library_id')
+ library_map, default_library = ctx.library_map
+ if library_id not in library_map:
+ library_id = default_library
+ db = get_db(ctx, library_id)
+ skeys = db.field_metadata.sortable_field_keys()
+ sorts, orders = [], []
+ for x in query.get('sort', '').split(','):
+ if x:
+ s, o = x.partition('.')[::2]
+ 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 = ['date'], ['desc']
+ return library_id, db, sorts, orders
-@endpoint('/ajax/interface-data/{library_id=None}', postprocess=json)
-def interface_data(ctx, rd, library_id):
+
+@endpoint('/ajax/interface-data', postprocess=json)
+def interface_data(ctx, rd):
'''
Return the data needed to create the server main UI
- Optional: ?num=50
+ Optional: ?num=50&sort=date.desc&library_id=
'''
- session = rd.session
- ans = {'session_data': {k:session[k] for k in defaults.iterkeys()}}
+ ans = {'username':rd.username}
ans['library_map'], ans['default_library'] = ctx.library_map
- ans['library_id'] = library_id or ans['default_library']
- sorts, orders = [], []
- for x in ans['session_data']['sort'].split(','):
- s, o = x.partition(':')[::2]
- sorts.append(s.strip()), orders.append(o.strip())
+ ans['library_id'], db, sorts, orders = get_basic_query_data(ctx, rd.query)
try:
num = int(rd.query.get('num', 50))
except Exception:
raise HTTPNotFound('Invalid number of books: %r' % rd.query.get('num'))
- db = get_db(ctx, library_id)
with db.safe_read_lock:
ans['search_result'] = _search(ctx, rd, db, '', num, 0, ','.join(sorts), ','.join(orders))
ans['field_metadata'] = db.field_metadata.all_metadata()
diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py
index 1a39d2fc9d..3cf0389fa4 100644
--- a/src/calibre/srv/code.py
+++ b/src/calibre/srv/code.py
@@ -4,10 +4,11 @@
from __future__ import (unicode_literals, division, absolute_import,
print_function)
-import re
+import re, json
from functools import partial
from threading import Lock
+from calibre import prepare_string_for_xml
from calibre.srv.routes import endpoint
html_cache = {}
@@ -43,4 +44,5 @@ def get_html(name, auto_reload_port, **replacements):
def index(ctx, rd):
return rd.generate_static_output('/', partial(
get_html, 'content-server/index.html', getattr(rd.opts, 'auto_reload_port', 0),
- ENTRY_POINT='book list', LOADING_MSG=_('Loading library, please wait')))
+ USERNAME=json.dumps(rd.username), ENTRY_POINT='book list',
+ LOADING_MSG=prepare_string_for_xml(_('Loading library, please wait'))))
diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py
index 5ad89d5e69..38a3187693 100644
--- a/src/calibre/srv/handler.py
+++ b/src/calibre/srv/handler.py
@@ -15,7 +15,6 @@ from threading import Lock
from calibre.db.cache import Cache
from calibre.db.legacy import create_backend, LibraryDatabase
from calibre.srv.routes import Router
-from calibre.srv.session import Sessions
from calibre.utils.date import utcnow
def init_library(library_path):
@@ -80,21 +79,18 @@ class Context(object):
url_for = None
CATEGORY_CACHE_SIZE = 25
SEARCH_CACHE_SIZE = 100
- SESSION_COOKIE = 'calibre_session'
def __init__(self, libraries, opts, testing=False):
self.opts = opts
self.library_broker = LibraryBroker(libraries)
self.testing = testing
self.lock = Lock()
- self.sessions = Sessions()
def init_session(self, endpoint, data):
- data.session = self.sessions.get_or_create(key=data.cookies.get(self.SESSION_COOKIE), username=data.username)
+ pass
def finalize_session(self, endpoint, data, output):
- data.outcookie[self.SESSION_COOKIE] = data.session.key
- data.outcookie[self.SESSION_COOKIE]['path'] = self.url_for(None)
+ pass
def get_library(self, library_id=None):
return self.library_broker.get(library_id)
diff --git a/src/calibre/srv/session.py b/src/calibre/srv/session.py
deleted file mode 100644
index f6109861a3..0000000000
--- a/src/calibre/srv/session.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python2
-# vim:fileencoding=utf-8
-# License: GPLv3 Copyright: 2015, Kovid Goyal
-
-from __future__ import (unicode_literals, division, absolute_import,
- print_function)
-from copy import deepcopy
-from uuid import uuid4
-from threading import Lock
-
-from calibre.utils.lru_cache import lru_cache
-
-defaults = {
- 'sort': 'date:desc',
- 'library_id': None,
- 'view_mode': 'cover_grid',
-}
-
-class Session(object):
-
- def __init__(self):
- self._data = deepcopy(defaults)
-
- def __getitem__(self, key):
- return self._data[key]
-
- def __setitem__(self, key, val):
- self._data[key] = val
-
-
-class SessionProxy(object):
-
- ''' Prevent the creation of a long-lived session object for every new
- request without a session cookie. Instead, this object lives only as long
- an individual request, and unless some setting is changed from the default
- simply returns values from the global defaults object. '''
-
- def __init__(self, sessions, key):
- self.sessions = sessions
- self.key = key
- self.actual_session = None
-
- def __getitem__(self, key):
- if self.actual_session is None:
- return defaults[key]
- return self.actual_session[key]
-
- def __setitem__(self, key, val):
- with self.sessions.lock:
- if self.actual_session is None:
- self.actual_session = self.sessions.cache[self.key] = Session()
- self.actual_session[key] = val
-
-class Sessions(object):
-
- def __init__(self):
- self.cache = lru_cache(size=2000)
- self.lock = Lock()
-
- def get_or_create(self, key=None, username=None):
- key = key or str(uuid4()).replace('-', '')
- try:
- with self.lock:
- return self.cache[key]
- except KeyError:
- return SessionProxy(self, key)
diff --git a/src/pyj/ajax.pyj b/src/pyj/ajax.pyj
index 7ade844f37..924a1ce6f1 100644
--- a/src/pyj/ajax.pyj
+++ b/src/pyj/ajax.pyj
@@ -1,10 +1,21 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal
-def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET'):
+def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None):
+ query = query or {}
xhr = XMLHttpRequest()
+ keys = Object.keys(query)
+ has_query = False
+ if keys.length:
+ for i, k in enumerate(keys):
+ val = query[k]
+ if val is undefined or val is None:
+ continue
+ path += ('?' if i == 0 else '&') + window.encodeURIComponent(k) + '=' + window.encodeURIComponent(val.toString())
+ has_query = True
if bypass_cache:
- path += ('&' if '?' in path else '?') + Date().getTime()
+ path += ('&' if has_query else '?') + Date().getTime()
+
xhr.request_path = path
def progress_callback(ev):
diff --git a/src/pyj/book_list/globals.pyj b/src/pyj/book_list/globals.pyj
index 34681493e8..ebc1b77bb9 100644
--- a/src/pyj/book_list/globals.pyj
+++ b/src/pyj/book_list/globals.pyj
@@ -2,11 +2,20 @@
# License: GPL v3 Copyright: 2015, Kovid Goyal
boss = None
+session_data = None
def get_boss():
- nonlocal boss
return boss
def set_boss(obj):
nonlocal boss
boss = obj
+ return boss
+
+def set_session_data(sd):
+ nonlocal session_data
+ session_data = sd
+ return session_data
+
+def get_session_data():
+ return session_data
diff --git a/src/pyj/book_list/item_list.pyj b/src/pyj/book_list/item_list.pyj
index f835f271f3..3eb35283f6 100644
--- a/src/pyj/book_list/item_list.pyj
+++ b/src/pyj/book_list/item_list.pyj
@@ -70,8 +70,9 @@ class ItemsView:
a = ul.lastChild.firstChild
if item.subtitle:
a.appendChild(E.div(item.subtitle, class_='subtitle'))
+ a.addEventListener('click', def(event): event.preventDefault();)
if item.action:
- a.addEventListener('click', def(event): event.preventDefault(), item.action();)
+ ul.lastChild.addEventListener('click', item.action)
def create_item(title, action=None, subtitle=None, icon_name=None):
diff --git a/src/pyj/book_list/views.pyj b/src/pyj/book_list/views.pyj
index a423a3a89b..3dbeae6512 100644
--- a/src/pyj/book_list/views.pyj
+++ b/src/pyj/book_list/views.pyj
@@ -4,11 +4,13 @@
from dom import set_css, build_rule
from elementmaker import E
from gettext import gettext as _
+from book_list.globals import get_session_data
-bv_counter = 0
THUMBNAIL_MAX_WIDTH = 300
THUMBNAIL_MAX_HEIGHT = 400
+bv_counter = 0
+
class BooksView:
def __init__(self, interface_data):
@@ -27,7 +29,7 @@ class BooksView:
E.div(id='get-more-books')
)
document.body.appendChild(div)
- self.set_view_mode(interface_data['session_data']['view_mode'])
+ self.set_view_mode(get_session_data().get('view_mode'))
def set_view_mode(self, mode='cover_grid'):
if self.mode == mode:
diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj
new file mode 100644
index 0000000000..2200a5d9d7
--- /dev/null
+++ b/src/pyj/session.pyj
@@ -0,0 +1,96 @@
+# vim:fileencoding=utf-8
+# License: GPL v3 Copyright: 2015, Kovid Goyal
+
+def storage_available(which):
+ which = which or 'localStorage'
+ try:
+ storage = window[which]
+ x = '__storage__test__'
+ storage.setItem(x, x)
+ storage.removeItem(x)
+ return True
+ except:
+ return False
+
+session_storage = None
+
+class FakeStorage:
+
+ def __init__(self):
+ self.data = {}
+
+ def getItem(self, key):
+ return self.data[key]
+
+ def setItem(self, key, value):
+ if type(value) != 'string':
+ value = JSON.stringify(value)
+ self.data[key] = value
+
+ def clear(self):
+ self.data = {}
+
+def get_session_storage():
+ nonlocal session_storage
+ if session_storage is None:
+ if storage_available('localStorage'):
+ session_storage = window.localStorage
+ elif storage_available('sessionStorage'):
+ session_storage = window.sessionStorage
+ console.error('localStorage not available using sessionStorage instead')
+ else:
+ session_storage = FakeStorage()
+ console.error('sessionStorage and localStorage not available using a temp cache instead')
+ return session_storage
+
+class SessionData:
+
+ def __init__(self):
+ self.storage = get_session_storage()
+ self.overflow_storage = {}
+ self.has_overflow = False
+
+ def get(self, key, defval):
+ if defval is undefined:
+ defval = None
+ if self.has_overflow:
+ ans = self.overflow_storage[key]
+ if ans is undefined:
+ ans = self.storage.getItem(key)
+ else:
+ ans = self.storage.getItem(key)
+ if ans is undefined:
+ return defval
+ return JSON.parse(ans)
+
+ def set(self, key, value):
+ value = JSON.stringify(value)
+ try:
+ self.storage.setItem(key, value)
+ v'delete self.overflow_storage[key]'
+ return True
+ except:
+ self.overflow_storage[key] = value
+ self.has_overflow = True
+ console.error('session storage has overflowed, using a temp cache instead')
+ return False
+
+ def clear(self):
+ self.storage.clear()
+ self.overflow_storage = {}
+ self.has_overflow = False
+
+class UserSessionData(SessionData):
+
+ def __init__(self, username):
+ self.prefix = (username or '') + ':'
+ self.has_user = bool(username)
+ self.username = username
+ SessionData.__init__(self)
+
+ def get(self, key, defval):
+ return SessionData.get(self, (self.prefix + key), defval)
+
+ def set(self, key, value):
+ return SessionData.set(self, (self.prefix + key), value)
+
diff --git a/src/pyj/srv.pyj b/src/pyj/srv.pyj
index d81764d019..a77be229e1 100644
--- a/src/pyj/srv.pyj
+++ b/src/pyj/srv.pyj
@@ -4,17 +4,19 @@
from ajax import ajax
from elementmaker import E
from gettext import gettext as _
+from session import UserSessionData
from book_list.boss import Boss
-from book_list.globals import set_boss
+from book_list.globals import set_boss, set_session_data
def on_library_loaded(end_type, xhr, ev):
p = document.getElementById('page_load_progress')
p.parentNode.removeChild(p)
if end_type == 'load':
- boss = Boss(JSON.parse(xhr.responseText))
+ interface_data = JSON.parse(xhr.responseText)
+ boss = Boss(interface_data)
set_boss(boss)
else:
- document.body.appendChild(E.p(style="color:red", str.format(_(
+ document.body.appendChild(E.p(style='color:red', str.format(_(
'Failed to download library data from "{}", with status: [{}] {}'),
xhr.request_path, xhr.status, xhr.statusText)))
@@ -23,11 +25,16 @@ def on_library_load_progress(loaded, total):
p.max = total
p.value = loaded
+def load_book_list():
+ sd = set_session_data(UserSessionData(window.calibre_username))
+ ajax('ajax/interface-data', on_library_loaded, on_library_load_progress, query={
+ 'library_id':sd.get('library_id'), 'sort':sd.get('sort')}).send()
+
def on_load():
if window.calibre_entry_point == 'book list':
- ajax('ajax/interface-data', on_library_loaded, on_library_load_progress).send()
+ load_book_list()
# We wait for all page elements to load, since this is a single page app
# with a largely empty starting document, we can use this to preload any resources
# we know are going to be needed immediately.
-window.addEventListener("load", on_load)
+window.addEventListener('load', on_load)