Work on downloading files for a rendered book

This commit is contained in:
Kovid Goyal 2016-03-20 20:49:42 +05:30
parent 43f03aa6c2
commit d96eb506bd
5 changed files with 157 additions and 21 deletions

View File

@ -69,7 +69,7 @@ class Container(ContainerBase):
self.virtualize_resources() self.virtualize_resources()
def manifest_data(name): def manifest_data(name):
return {'size':os.path.getsize(self.name_path_map[name]), 'is_virtualized': name in self.virtualized_names} return {'size':os.path.getsize(self.name_path_map[name]), 'is_virtualized': name in self.virtualized_names}
data['manifest'] = {name:manifest_data(name) for name in set(self.name_path_map) - excluded_names}, data['files'] = {name:manifest_data(name) for name in set(self.name_path_map) - excluded_names}
self.commit() self.commit()
for name in excluded_names: for name in excluded_names:
os.remove(self.name_path_map[name]) os.remove(self.name_path_map[name])

View File

@ -18,7 +18,7 @@ def encode_query(query):
has_query = True has_query = True
return path return path
def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200): def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', query=None, timeout=30*1000, ok_code=200, progress_totals_needed=True):
# Run an AJAX request. on_complete must be a function that accepts three # Run an AJAX request. on_complete must be a function that accepts three
# arguments: end_type, xhr, ev where end_type is one of 'abort', 'error', # arguments: end_type, xhr, ev where end_type is one of 'abort', 'error',
# 'load', 'timeout'. In case end_type is anything other than 'load' you can # 'load', 'timeout'. In case end_type is anything other than 'load' you can
@ -51,6 +51,9 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q
if ev.lengthComputable: if ev.lengthComputable:
on_progress(ev.loaded, ev.total, xhr) on_progress(ev.loaded, ev.total, xhr)
elif ev.loaded: elif ev.loaded:
if not progress_totals_needed:
on_progress(ev.loaded, undefined, xhr)
return
ul = xhr.getResponseHeader('Calibre-Uncompressed-Length') ul = xhr.getResponseHeader('Calibre-Uncompressed-Length')
if ul: if ul:
try: try:

View File

@ -2,7 +2,6 @@
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from gettext import gettext as _ from gettext import gettext as _
from modals import error_dialog
def upgrade_schema(idb, old_version, new_version): def upgrade_schema(idb, old_version, new_version):
print('upgrade_schema:', old_version, new_version) print('upgrade_schema:', old_version, new_version)
@ -21,6 +20,7 @@ class DB:
self.supports_blobs = supports_blobs self.supports_blobs = supports_blobs
if not supports_blobs: if not supports_blobs:
print('IndexedDB does not support Blob storage, using base64 encoding instead') print('IndexedDB does not support Blob storage, using base64 encoding instead')
self.show_error = ui.show_error.bind(ui)
idb.onerror = def(event): idb.onerror = def(event):
self.display_error(None, event) self.display_error(None, event)
@ -31,7 +31,7 @@ class DB:
idb.onversionchange = def(event): idb.onversionchange = def(event):
idb.close() idb.close()
error_dialog(_('Database upgraded!'), _( ui.show_error(_('Database upgraded!'), _(
'A newer version of calibre is available, please click the reload button in your browser.')) 'A newer version of calibre is available, please click the reload button in your browser.'))
def display_error(self, msg, event): def display_error(self, msg, event):
@ -46,7 +46,7 @@ class DB:
desc = desc.error.toString() desc = desc.error.toString()
elif desc.errorCode: elif desc.errorCode:
desc = desc.errorCode desc = desc.errorCode
error_dialog(_('Cannot read book'), msg, desc) self.show_error(_('Cannot read book'), msg, desc)
def do_op(self, stores, data, error_msg, proceed, op='get', store=None): def do_op(self, stores, data, error_msg, proceed, op='get', store=None):
store = store or stores[0] store = store or stores[0]
@ -71,7 +71,7 @@ class DB:
proceed(result or { proceed(result or {
'key':key, 'key':key,
'is_complete':False, 'is_complete':False,
'stores_blobs': True, 'b64_encoded_files': v'[]',
'book_hash':None, 'book_hash':None,
'last_read': Date(), 'last_read': Date(),
'metadata': metadata, 'metadata': metadata,
@ -84,10 +84,14 @@ class DB:
book.metadata = manifest.metadata book.metadata = manifest.metadata
book.book_hash = manifest.book_hash.hash book.book_hash = manifest.book_hash.hash
book.is_complete = False book.is_complete = False
book.stores_blobs = True book.b64_encoded_files = v'[]'
book.is_complete = False
v'delete manifest["metadata"]' v'delete manifest["metadata"]'
self.do_op(['books'], book, _('Failed to write to the books database'), proceed, op='put') self.do_op(['books'], book, _('Failed to write to the books database'), proceed, op='put')
def store_file(self, book, fname, xhr):
pass
def create_db(ui, interface_data): def create_db(ui, interface_data):
if not window.indexedDB: if not window.indexedDB:
ui.db = _('Your browser does not support IndexedDB. Cannot read books. Consider using a modern browser, such as Firefox, Chrome or Edge.') ui.db = _('Your browser does not support IndexedDB. Cannot read books. Consider using a modern browser, such as Firefox, Chrome or Edge.')

View File

@ -1,9 +1,11 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from ajax import ajax from ajax import ajax, encode_query
from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from modals import error_dialog from modals import error_dialog
from utils import human_readable
from read_book.db import create_db from read_book.db import create_db
RENDER_VERSION = 1 # Also change this in render_book.py RENDER_VERSION = 1 # Also change this in render_book.py
@ -13,10 +15,71 @@ class ReadUI:
def __init__(self, interface_data, container): def __init__(self, interface_data, container):
self.interface_data = interface_data self.interface_data = interface_data
self.db = None self.db = None
self.current_metadata = None self.current_metadata = {'title': _('Unknown book')}
self.current_book_id = None
self.manifest_xhr = None self.manifest_xhr = None
create_db(self, interface_data) create_db(self, interface_data)
self.pending_load = None self.pending_load = None
self.downloads_in_progress = []
self.progress_id = 'book-load-progress'
self.display_id = 'book-iframe-container'
self.error_id = 'book-global-error-container'
self.stacked_widgets = [self.progress_id, self.display_id, self.error_id]
container.appendChild(E.div(
id=self.progress_id, style='display:none; text-align: center',
E.h3(style='margin-top:30vh; margin-bottom: 1ex;'),
E.progress(style='margin: 1ex'),
E.div(style='margin: 1ex')
))
container.appendChild(E.div(
id=self.error_id, style='display:none; text-align: center',
E.h2(_('Could not open book'), style='margin: 1ex; margin-top: 30vh;'),
E.div(style='margin: 1ex', E.a()),
))
container.appendChild(E.div(
id=self.display_id, style='display:none',
))
def show_stack(self, name):
ans = None
for w in self.stacked_widgets:
d = document.getElementById(w)
v = 'none'
if name is w:
ans = d
v = 'block'
d.style.display = v
return ans
def show_error(self, title, msg, details):
div = self.show_stack(self.error_id)
error_dialog(title, msg, details)
a = div.lastChild.firstChild
if self.current_book_id:
a.setAttribute('href', encode_query({
'library_id':self.interface_data.library_id,
'book-id': (self.current_book_id + ''),
'panel':'book-details'
}))
a.textContent = str.format(_(
'Click to go back to the details page for: {}'), self.current_metadata.title)
else:
a.textContent = ''
a.setAttribute('href', '')
def init_ui(self):
div = self.show_stack(self.progress_id)
if self.current_metadata:
div.firstChild.textContent = str.format(_(
'Downloading {0} for offline reading, please wait...'), self.current_metadata.title)
else:
div.firstChild.textContent = ''
pr = div.firstChild.nextSibling
pr.removeAttribute('value'), pr.removeAttribute('max')
div.lastChild.textContent = _('Downloading book manifest...')
def load_book(self, book_id, fmt, metadata): def load_book(self, book_id, fmt, metadata):
if self.db is None: if self.db is None:
@ -31,18 +94,22 @@ class ReadUI:
self.start_load(*pl) self.start_load(*pl)
def start_load(self, book_id, fmt, metadata): def start_load(self, book_id, fmt, metadata):
if type(self.db) is 'string': self.current_book_id = book_id
error_dialog(_('Cannot read book'), self.db)
return
metadata = metadata or self.interface_data.metadata[book_id] metadata = metadata or self.interface_data.metadata[book_id]
self.current_metadata = metadata or {'title':_('Current Book')} self.current_metadata = metadata or {'title':_('Book id #') + book_id}
self.init_ui()
if type(self.db) is 'string':
self.show_error(_('Cannot read book'), self.db)
return
self.db.get_book(book_id, fmt, metadata, self.got_book.bind(self)) self.db.get_book(book_id, fmt, metadata, self.got_book.bind(self))
def got_book(self, book): def got_book(self, book):
if not book.manifest or book.manifest.version != RENDER_VERSION: if not book.manifest or book.manifest.version != RENDER_VERSION or not book.is_complete:
# We re-download the manifest when the book is not complete to ensure we have the
# correct manifest, even though doing so is not strictly necessary
self.get_manifest(book) self.get_manifest(book)
return else:
self.display_book(book) if book.is_complete else self.download_book(book) self.display_book(book)
def get_manifest(self, book): def get_manifest(self, book):
library_id, book_id, fmt = book.key library_id, book_id, fmt = book.key
@ -58,23 +125,70 @@ class ReadUI:
if end_type is 'abort': if end_type is 'abort':
return return
if end_type is not 'load': if end_type is not 'load':
return error_dialog(_('Failed to load book manifest'), str.format( return self.show_error(_('Failed to load book manifest'), str.format(
_('Could not open {title} as book manifest failed to load, click "Show Details" for more information.'), title=self.current_metadata.title), _('Could not open {title} as book manifest failed to load, click "Show Details" for more information.'), title=self.current_metadata.title),
xhr.error_html) xhr.error_html)
try: try:
manifest = JSON.parse(xhr.responseText) manifest = JSON.parse(xhr.responseText)
except Exception as err: except Exception as err:
return error_dialog(_('Failed to load book manifest'), str.format( return self.show_error(_('Failed to load book manifest'), str.format(
_('The manifest for {title} is not valid'), title=self.current_metadata.title), _('The manifest for {title} is not valid'), title=self.current_metadata.title),
err.stack or err.toString()) err.stack or err.toString())
if manifest.version != RENDER_VERSION: if manifest.version != RENDER_VERSION:
return error_dialog(_('calibre upgraded!'), _( return self.show_error(_('calibre upgraded!'), _(
'A newer version of calibre is available, please click the reload button in your browser.')) 'A newer version of calibre is available, please click the reload button in your browser.'))
self.current_metadata = manifest.metadata self.current_metadata = manifest.metadata
self.db.save_manifest(book, manifest, self.download_book.bind(self, book)) self.db.save_manifest(book, manifest, self.download_book.bind(self, book))
def download_book(self, book): def download_book(self, book):
pass files = book.manifest.files
total = 0
for name in files:
total += files[name].size
files_left = set(book.manifest.files)
failed_files = []
for xhr in self.downloads_in_progress:
xhr.abort()
self.downloads_in_progress = []
progress = document.getElementById(self.progress_id)
pbar = progress.firstChild.nextSibling
library_id, book_id, fmt = book.key
base_path = str.format(
'book-file/{}/{}/{}/{}/', encodeURIComponent(book_id), encodeURIComponent(fmt),
encodeURIComponent(book.manifest.book_hash.size), encodeURIComponent(book.manifest.book_hash.mtime))
query = {'library_id': library_id}
progress_track = {}
pbar.setAttribute('max', total + '')
def update_progress():
x = 0
for name in progress_track:
x += progress_track[name]
pbar.setAttribute('value', x + '')
progress.lastChild.textContent = str.format(_('Downloaded {0}, {1} left'), human_readable(x), human_readable(total - x))
def on_complete(end_type, xhr, ev):
self.downloads_in_progress.remove(xhr)
files_left.discard(this)
progress_track[this] = files[this].size
update_progress()
if end_type is 'abort':
return
if end_type is 'load':
self.db.store_file(book, fname, xhr)
else:
failed_files.append([this, xhr.error_html])
if len(files_left):
return
def on_progress(loaded):
progress_track[this] = loaded
update_progress()
for fname in files_left:
xhr = ajax(base_path + encodeURIComponent(fname), on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=False)
xhr.send()
self.downloads_in_progress.append(xhr)
def display_book(self, book): def display_book(self, book):
pass self.show_stack(self.display_id)

View File

@ -79,5 +79,20 @@ def fmt_sidx(val, fmt='{:.2f}', use_roman=True):
return int(val) + '' return int(val) + ''
return str.format(fmt, float(val)) return str.format(fmt, float(val))
def human_readable(size, sep=' '):
divisor, suffix = 1, "B"
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
if size < (1 << ((i + 1) * 10)):
divisor, suffix = (1 << (i * 10)), candidate
break
size = (float(size)/divisor) + ''
pos = str.find(size, ".")
if pos > -1:
size = size[:pos + 2]
if str.endswith(size, '.0'):
size = size[:-2]
return size + sep + suffix
if __name__ is '__main__': if __name__ is '__main__':
print(fmt_sidx(10), fmt_sidx(1.2)) print(fmt_sidx(10), fmt_sidx(1.2))
print(list(map(human_readable, [1, 1024.0, 1025, 1024*1024*2.3])))