pull/1885/head
cbartondock 3 years ago
commit d0671ec58c

@ -22,7 +22,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Support for public user registration
- Send eBooks to Kindle devices with the click of a button
- Sync your Kobo devices through Calibre-Web with your Calibre library
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user

@ -37,6 +37,7 @@ try:
except ImportError:
from flask_login.__about__ import __version__ as flask_loginVersion
try:
# pylint: disable=unused-import
import unidecode
# _() necessary to make babel aware of string for translation
unidecode_version = _(u'installed')

@ -49,7 +49,7 @@ def init_cache_busting(app):
# compute version component
rooted_filename = os.path.join(dirpath, filename)
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
@ -64,6 +64,7 @@ def init_cache_busting(app):
return filename.split("?", 1)[0]
@app.url_defaults
# pylint: disable=unused-variable
def reverse_to_cache_busted_url(endpoint, values):
"""
Make `url_for` produce busted filenames when using the 'static' endpoint.

@ -104,7 +104,7 @@ LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123" # nosec # noqa
DEFAULT_PASSWORD = "admin123" # nosec
DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try:

@ -211,31 +211,8 @@ def delete_book_from_details(book_id):
def delete_book_ajax(book_id, book_format):
return delete_book(book_id,book_format, False)
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning")
if not book_format:
def delete_whole_book(book_id, book):
# delete book from Shelfs, Downloads, Read list
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
@ -279,16 +256,9 @@ def delete_book(book_id, book_format, jsonResponse):
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
calibre_db.session, 'custom')
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
calibre_db.session.commit()
except Exception as e:
log.debug_or_exception(e)
calibre_db.session.rollback()
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
def render_delete_book_result(book_format, jsonResponse, warning, book_id):
if book_format:
if jsonResponse:
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
@ -309,6 +279,45 @@ def delete_book(book_id, book_format, jsonResponse):
return redirect(url_for('web.index'))
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning")
if not book_format:
delete_whole_book(book_id, book)
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
calibre_db.session.commit()
except Exception as e:
log.debug_or_exception(e)
calibre_db.session.rollback()
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
def render_edit_book(book_id):
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
@ -447,18 +456,8 @@ def edit_book_publisher(to_save, book):
return changed
def edit_cc_data(book_id, book, to_save):
def edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string):
changed = False
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
cc_db_value = getattr(book, cc_string)[0].value
else:
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
if to_save[cc_string] == 'None':
to_save[cc_string] = None
elif c.datatype == 'bool':
@ -479,8 +478,11 @@ def edit_cc_data(book_id, book, to_save):
new_cc = cc_class(value=to_save[cc_string], book=book_id)
calibre_db.session.add(new_cc)
changed = True
return changed, to_save
else:
def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
changed = False
if c.datatype == 'rating':
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
if to_save[cc_string].strip() != cc_db_value:
@ -504,6 +506,24 @@ def edit_cc_data(book_id, book, to_save):
cc_class.value == to_save[cc_string].strip()).first()
# add cc value to book
getattr(book, cc_string).append(new_cc)
return changed, to_save
def edit_cc_data(book_id, book, to_save):
changed = False
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
if len(getattr(book, cc_string)) > 0:
cc_db_value = getattr(book, cc_string)[0].value
else:
cc_db_value = None
if to_save[cc_string].strip():
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
changed, to_save = edit_cc_data_number(book_id, book, c, to_save, cc_db_value, cc_string)
else:
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
else:
if cc_db_value is not None:
# remove old cc_val
@ -766,6 +786,7 @@ def merge_metadata(to_save, meta):
to_save["description"] = to_save["description"] or Markup(
getattr(meta, 'description', '')).unescape()
def identifier_list(to_save, book):
"""Generate a list of Identifiers from form information"""
id_type_prefix = 'identifier-type-'
@ -780,43 +801,8 @@ def identifier_list(to_save, book):
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
return result
@editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano
@upload_required
def upload():
if not config.config_uploading:
abort(404)
if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"):
try:
modif_date = False
# create the function for sorting...
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
title = meta.title
authr = meta.author
def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'):
entry = calibre_db.check_exists_book(authr, title)
if entry:
@ -851,12 +837,20 @@ def upload():
sort_author = stored_author.sort
sort_authors_list.append(sort_author)
sort_authors = ' & '.join(sort_authors_list)
return sort_authors, input_authors, db_author
def create_book_on_upload(modif_date, meta):
title = meta.title
authr = meta.author
sort_authors, input_authors, db_author = prepare_authors_on_upload(title, authr)
title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(db_author.name)
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
@ -884,6 +878,44 @@ def upload():
# flush content, get db_book.id available
calibre_db.session.flush()
return db_book, input_authors, title_dir
@editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano
@upload_required
def upload():
if not config.config_uploading:
abort(404)
if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"):
try:
modif_date = False
# create the function for sorting...
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta)
# Comments needs book id therfore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)

@ -60,14 +60,8 @@ def init_errorhandler():
if services.ldap:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
# pylint: disable=unused-variable
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))

@ -142,7 +142,7 @@ def on_received_watch_confirmation():
else:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)

@ -137,43 +137,8 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
return
def check_send_to_kindle_without_converter(entry):
def check_send_to_kindle_with_converter(formats):
bookformats = list()
# no converter - only for mobi and pdf formats
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
if 'MOBI' in ele.format:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in ele.format:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in ele.format:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
return bookformats
def check_send_to_kindle_with_converter(entry):
bookformats = list()
formats = list()
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert': 1,
@ -193,12 +158,27 @@ def check_send_to_kindle(entry):
"""
returns all available book formats for sending to Kindle
"""
formats = list()
bookformats = list()
if len(entry.data):
if not config.config_converterpath:
book_formats = check_send_to_kindle_with_converter(entry)
else:
book_formats = check_send_to_kindle_with_converter(entry)
return book_formats
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if config.config_converterpath:
bookformats.extend(check_send_to_kindle_with_converter(formats))
return bookformats
else:
log.error(u'Cannot find book entry %d', entry.id)
return None
@ -207,7 +187,7 @@ def check_send_to_kindle(entry):
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
# list with supported formats
def check_read_formats(entry):
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
bookformats = list()
if len(entry.data):
for ele in iter(entry.data):
@ -750,7 +730,7 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist):
renderedtasklist = list()
for __, user, added, task in tasklist:
for __, user, __, task in tasklist:
if user == current_user.nickname or current_user.role_admin():
ret = {}
if task.start_time:

@ -918,7 +918,7 @@ def HandleAuthRequest():
if config.config_kobo_proxy:
try:
return redirect_or_proxy_request()
except:
except Exception:
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
return make_calibre_web_auth_response()

@ -81,6 +81,7 @@ log = logger.create()
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
# pylint: disable=unused-variable
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")

@ -96,85 +96,6 @@ def logout_oauth_user():
session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support:
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = provider
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session_commit("{} Blueprint Created".format(provider))
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1 = dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2 = dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://console.developers.google.com/apis/credentials')
oauthblueprints.append(ele1)
oauthblueprints.append(ele2)
for element in oauthblueprints:
if element['provider_name'] == 'github':
blueprint_func = make_github_blueprint
else:
blueprint_func = make_google_blueprint
blueprint = blueprint_func(
client_id=element['oauth_client_id'],
client_secret=element['oauth_client_secret'],
redirect_to="oauth."+element['provider_name']+"_login",
scope=element['scope']
)
element['blueprint'] = blueprint
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
return False
github_info = resp.json()
github_user_id = str(github_info["id"])
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
return False
google_info = resp.json()
google_user_id = str(google_info["id"])
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
@ -281,6 +202,88 @@ if ub.oauth_support:
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
def generate_oauth_blueprints():
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
oauthProvider.provider_name = provider
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session_commit("{} Blueprint Created".format(provider))
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1 = dict(provider_name='github',
id=oauth_ids[0].id,
active=oauth_ids[0].active,
oauth_client_id=oauth_ids[0].oauth_client_id,
scope=None,
oauth_client_secret=oauth_ids[0].oauth_client_secret,
obtain_link='https://github.com/settings/developers')
ele2 = dict(provider_name='google',
id=oauth_ids[1].id,
active=oauth_ids[1].active,
scope=["https://www.googleapis.com/auth/userinfo.email"],
oauth_client_id=oauth_ids[1].oauth_client_id,
oauth_client_secret=oauth_ids[1].oauth_client_secret,
obtain_link='https://console.developers.google.com/apis/credentials')
oauthblueprints.append(ele1)
oauthblueprints.append(ele2)
for element in oauthblueprints:
if element['provider_name'] == 'github':
blueprint_func = make_github_blueprint
else:
blueprint_func = make_google_blueprint
blueprint = blueprint_func(
client_id=element['oauth_client_id'],
client_secret=element['oauth_client_secret'],
redirect_to="oauth."+element['provider_name']+"_login",
scope=element['scope']
)
element['blueprint'] = blueprint
element['blueprint'].backend = OAuthBackend(ub.OAuth, ub.session, str(element['id']),
user=current_user, user_required=True)
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
return oauthblueprints
if ub.oauth_support:
oauthblueprints = generate_oauth_blueprints()
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
return False
github_info = resp.json()
github_user_id = str(github_info["id"])
return oauth_update_token(str(oauthblueprints[0]['id']), token, github_user_id)
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
return False
google_info = resp.json()
google_user_id = str(google_info["id"])
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
# notify on OAuth provider error
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])

@ -22,7 +22,7 @@ import os
import errno
import signal
import socket
import subprocess
import subprocess # nosec
try:
from gevent.pywsgi import WSGIServer
@ -253,13 +253,13 @@ class WebServer(object):
if not self.restart:
log.info("Performing shutdown of Calibre-Web")
# prevent irritiating log of pending tasks message from asyncio
# prevent irritating log of pending tasks message from asyncio
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
return True
log.info("Performing restart of Calibre-Web")
args = self._get_args_for_reloading()
subprocess.call(args, close_fds=True)
subprocess.call(args, close_fds=True) # nosec
return True
def _killServer(self, __, ___):

@ -22,6 +22,7 @@ from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from datetime import datetime
try:
# pylint: disable=unused-import
from urllib import unquote
except ImportError:
from urllib.parse import unquote
@ -91,14 +92,14 @@ class SyncToken:
def __init__(
self,
raw_kobo_store_token="", # nosec
raw_kobo_store_token="",
books_last_created=datetime.min,
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
books_last_id=-1
):
): # nosec
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified

@ -1783,6 +1783,12 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover {
margin-top: 0
}
.container-fluid .book .meta .series {
/* font-weight: 400; */
/* font-size: 12px; */
color: hsla(0, 0%, 100%, .45);
}
.container-fluid .book .meta > p {
-o-text-overflow: ellipsis;
text-overflow: ellipsis;

@ -0,0 +1,194 @@
body {
margin: 0px;
}
#djvuContainer {
position: absolute;
width: 100%;
height: 100%;
max-width: 100%;
text-align: center;
overflow: hidden;
}
.toolbar {
position: relative;
display: inline-block;
padding-top: 10px;
transform: translate(0, 0);
-webkit-transform: translate(0, 0);
-ms-transform: translate(0, 0);
transition: transform 0.3s;
-webkit-transition: -webkit-transform 0.3s;
}
.toolbarHidden {
transform: translate(0, -100%);
-webkit-transform: translate(0, -100%);
-ms-transform: translate(0, -100%);
transition: transform 1s;
-webkit-transition: transform 1s;
}
.toolbarSquareButton {
float: left;
width: 40px;
height: 40px;
background-image: url("img/toolbar-buttons.png");
background-repeat: no-repeat;
background-size: 500% 300%;
}
.scrollbar {
position: absolute;
border-radius: 6px;
opacity: 0.6;
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #BBB;
transition: opacity 0.3s;
}
.scrollbar:hover {
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #999;
}
.scrollbarClicked, .scrollbarClicked:hover {
box-shadow: inset 0 0 0 1px black, inset 0 0 0 2px white, inset 0 0 0 10px #777;
}
.scrollbarHidden {
opacity: 0;
transition: opacity 0.6s;
}
.scrollbarVertical {
right: 0px;
border-right: 1px solid transparent;
width: 13px;
}
.scrollbarHorizontal {
bottom: 0px;
border-bottom: 1px solid transparent;
height: 13px;
}
.content {
overflow: hidden;
position: absolute;
height: 100%;
width: 100%;
}
.textLayer {
position: absolute;
height: 120%;
width: 120%;
overflow: scroll;
text-align: left;
}
_:-ms-lang(x), .textLayer {
height: 100%;
width: 100%;
-ms-overflow-style: none;
}
.textPage {
margin-top: 100vh;
margin-bottom: 100vh;
padding-right: 100vw;
}
.textPage span {
font-family: sans-serif;
color: #000;
color: rgba(0, 0, 0, 0);
white-space: nowrap;
}
.visibleTextPage span {
display: inline-block;
position: relative;
top: 50%;
transform: translateY(-50%);
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
/* border: 1px solid red; /* for easy debug */
}
.buttonZoomIn {
background-position: 25% 0;
}
.buttonZoomIn:hover {
background-position: 25% 50%;
}
.buttonZoomIn:disabled {
background-position: 25% 100%;
}
.buttonZoomOut {
background-position: 0 0;
}
.buttonZoomOut:hover {
background-position: 0 50%;
}
.buttonZoomOut:disabled {
background-position: 0 100%;
}
.buttonPagePrev {
background-position: 50% 0;
}
.buttonPagePrev:hover {
background-position: 50% 50%;
}
.buttonPagePrev:disabled {
background-position: 50% 100%;
}
.buttonPageNext {
background-position: 75% 0;
}
.buttonPageNext:hover {
background-position: 75% 50%;
}
.buttonPageNext:disabled {
background-position: 75% 100%;
}
.toolbarItem {
display: inline-block;
margin: 0 10px;
}
.comboBox {
float: left;
position: relative;
}
.comboBoxSelection {
width: 8.25ex;
margin: 10px 12px 0px 12px;
}
.comboBoxText {
width: 5ex;
border: none;
padding: 0px;
outline: none;
position: absolute;
margin: 10px 0px 0px 12px;
top: 2px;
left: 3px;
}
.statusImage {
position: absolute;
left: 50%;
top: 50%;
width: 128px;
height: 128px;
margin: -72px 0 0 -64px;
background-image: url("img/status.png");
background-repeat: no-repeat;
}
.blankImage {
background-image: url("img/blank.jpg");
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

@ -0,0 +1,16 @@
091ECB3AE852C68866FBC86AA8FCDB1F.cache.js
user.agent ie8
1A420474460884E73E3288F783AD7024.cache.js
user.agent ie10
3CFDA9D9AFA2798299BBAE243DB2E9B5.cache.js
user.agent safari
564CB406D925C79CCD67EB98CA5AD4EF.cache.js
user.agent gecko1_8
A8305F17E026239876FCBC730B035A55.cache.js
user.agent ie9
Devmode:devmode.js

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
function djvu_html5(){var O='bootstrap',P='begin',Q='gwt.codesvr.djvu_html5=',R='gwt.codesvr=',S='djvu_html5',T='startup',U='DUMMY',V=0,W=1,X='iframe',Y='javascript:""',Z='position:absolute; width:0; height:0; border:none; left: -1000px;',$=' top: -1000px;',_='CSS1Compat',ab='<!doctype html>',bb='',cb='<html><head><\/head><body><\/body><\/html>',db='undefined',eb='readystatechange',fb=10,gb='Chrome',hb='eval("',ib='");',jb='script',kb='javascript',lb='moduleStartup',mb='moduleRequested',nb='Failed to load ',ob='head',pb='meta',qb='name',rb='djvu_html5::',sb='::',tb='gwt:property',ub='content',vb='=',wb='gwt:onPropertyErrorFn',xb='Bad handler "',yb='" for "gwt:onPropertyErrorFn"',zb='gwt:onLoadErrorFn',Ab='" for "gwt:onLoadErrorFn"',Bb='#',Cb='?',Db='/',Eb='img',Fb='clear.cache.gif',Gb='baseUrl',Hb='djvu_html5.nocache.js',Ib='base',Jb='//',Kb='user.agent',Lb='webkit',Mb='safari',Nb='msie',Ob=11,Pb='ie10',Qb=9,Rb='ie9',Sb=8,Tb='ie8',Ub='gecko',Vb='gecko1_8',Wb=2,Xb=3,Yb=4,Zb='selectingPermutation',$b='djvu_html5.devmode.js',_b='091ECB3AE852C68866FBC86AA8FCDB1F',ac='1A420474460884E73E3288F783AD7024',bc='3CFDA9D9AFA2798299BBAE243DB2E9B5',cc='564CB406D925C79CCD67EB98CA5AD4EF',dc='A8305F17E026239876FCBC730B035A55',ec=':',fc='.cache.js',gc='loadExternalRefs',hc='end',ic='http:',jc='file:',kc='_gwt_dummy_',lc='__gwtDevModeHook:djvu_html5',mc='Ignoring non-whitelisted Dev Mode URL: ',nc=':moduleBase';var o=window;var p=document;r(O,P);function q(){var a=o.location.search;return a.indexOf(Q)!=-1||a.indexOf(R)!=-1}
function r(a,b){if(o.__gwtStatsEvent){o.__gwtStatsEvent({moduleName:S,sessionId:o.__gwtStatsSessionId,subSystem:T,evtGroup:a,millis:(new Date).getTime(),type:b})}}
djvu_html5.__sendStats=r;djvu_html5.__moduleName=S;djvu_html5.__errFn=null;djvu_html5.__moduleBase=U;djvu_html5.__softPermutationId=V;djvu_html5.__computePropValue=null;djvu_html5.__getPropMap=null;djvu_html5.__installRunAsyncCode=function(){};djvu_html5.__gwtStartLoadingFragment=function(){return null};djvu_html5.__gwt_isKnownPropertyValue=function(){return false};djvu_html5.__gwt_getMetaProperty=function(){return null};var s=null;var t=o.__gwt_activeModules=o.__gwt_activeModules||{};t[S]={moduleName:S};djvu_html5.__moduleStartupDone=function(e){var f=t[S].bindings;t[S].bindings=function(){var a=f?f():{};var b=e[djvu_html5.__softPermutationId];for(var c=V;c<b.length;c++){var d=b[c];a[d[V]]=d[W]}return a}};var u;function v(){w();return u}
function w(){if(u){return}var a=p.createElement(X);a.src=Y;a.id=S;a.style.cssText=Z+$;a.tabIndex=-1;p.body.appendChild(a);u=a.contentDocument;if(!u){u=a.contentWindow.document}u.open();var b=document.compatMode==_?ab:bb;u.write(b+cb);u.close()}
function A(k){function l(a){function b(){if(typeof p.readyState==db){return typeof p.body!=db&&p.body!=null}return /loaded|complete/.test(p.readyState)}
var c=b();if(c){a();return}function d(){if(!c){if(!b()){return}c=true;a();if(p.removeEventListener){p.removeEventListener(eb,d,false)}if(e){clearInterval(e)}}}
if(p.addEventListener){p.addEventListener(eb,d,false)}var e=setInterval(function(){d()},fb)}
function m(c){function d(a,b){a.removeChild(b)}
var e=v();var f=e.body;var g;if(navigator.userAgent.indexOf(gb)>-1&&window.JSON){var h=e.createDocumentFragment();h.appendChild(e.createTextNode(hb));for(var i=V;i<c.length;i++){var j=window.JSON.stringify(c[i]);h.appendChild(e.createTextNode(j.substring(W,j.length-W)))}h.appendChild(e.createTextNode(ib));g=e.createElement(jb);g.language=kb;g.appendChild(h);f.appendChild(g);d(f,g)}else{for(var i=V;i<c.length;i++){g=e.createElement(jb);g.language=kb;g.text=c[i];f.appendChild(g);d(f,g)}}}
djvu_html5.onScriptDownloaded=function(a){l(function(){m(a)})};r(lb,mb);var n=p.createElement(jb);n.src=k;if(djvu_html5.__errFn){n.onerror=function(){djvu_html5.__errFn(S,new Error(nb+code))}}p.getElementsByTagName(ob)[V].appendChild(n)}
djvu_html5.__startLoadingFragment=function(a){return D(a)};djvu_html5.__installRunAsyncCode=function(a){var b=v();var c=b.body;var d=b.createElement(jb);d.language=kb;d.text=a;c.appendChild(d);c.removeChild(d)};function B(){var c={};var d;var e;var f=p.getElementsByTagName(pb);for(var g=V,h=f.length;g<h;++g){var i=f[g],j=i.getAttribute(qb),k;if(j){j=j.replace(rb,bb);if(j.indexOf(sb)>=V){continue}if(j==tb){k=i.getAttribute(ub);if(k){var l,m=k.indexOf(vb);if(m>=V){j=k.substring(V,m);l=k.substring(m+W)}else{j=k;l=bb}c[j]=l}}else if(j==wb){k=i.getAttribute(ub);if(k){try{d=eval(k)}catch(a){alert(xb+k+yb)}}}else if(j==zb){k=i.getAttribute(ub);if(k){try{e=eval(k)}catch(a){alert(xb+k+Ab)}}}}}__gwt_getMetaProperty=function(a){var b=c[a];return b==null?null:b};s=d;djvu_html5.__errFn=e}
function C(){function e(a){var b=a.lastIndexOf(Bb);if(b==-1){b=a.length}var c=a.indexOf(Cb);if(c==-1){c=a.length}var d=a.lastIndexOf(Db,Math.min(c,b));return d>=V?a.substring(V,d+W):bb}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=p.createElement(Eb);b.src=a+Fb;a=e(b.src)}return a}
function g(){var a=__gwt_getMetaProperty(Gb);if(a!=null){return a}return bb}
function h(){var a=p.getElementsByTagName(jb);for(var b=V;b<a.length;++b){if(a[b].src.indexOf(Hb)!=-1){return e(a[b].src)}}return bb}
function i(){var a=p.getElementsByTagName(Ib);if(a.length>V){return a[a.length-W].href}return bb}
function j(){var a=p.location;return a.href==a.protocol+Jb+a.host+a.pathname+a.search+a.hash}
var k=g();if(k==bb){k=h()}if(k==bb){k=i()}if(k==bb&&j()){k=e(p.location.href)}k=f(k);return k}
function D(a){if(a.match(/^\//)){return a}if(a.match(/^[a-zA-Z]+:\/\//)){return a}return djvu_html5.__moduleBase+a}
function F(){var f=[];var g=V;function h(a,b){var c=f;for(var d=V,e=a.length-W;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
var i=[];var j=[];function k(a){var b=j[a](),c=i[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(s){s(a,d,b)}throw null}
j[Kb]=function(){var a=navigator.userAgent.toLowerCase();var b=p.documentMode;if(function(){return a.indexOf(Lb)!=-1}())return Mb;if(function(){return a.indexOf(Nb)!=-1&&(b>=fb&&b<Ob)}())return Pb;if(function(){return a.indexOf(Nb)!=-1&&(b>=Qb&&b<Ob)}())return Rb;if(function(){return a.indexOf(Nb)!=-1&&(b>=Sb&&b<Ob)}())return Tb;if(function(){return a.indexOf(Ub)!=-1||b>=Ob}())return Vb;return bb};i[Kb]={'gecko1_8':V,'ie10':W,'ie8':Wb,'ie9':Xb,'safari':Yb};__gwt_isKnownPropertyValue=function(a,b){return b in i[a]};djvu_html5.__getPropMap=function(){var a={};for(var b in i){if(i.hasOwnProperty(b)){a[b]=k(b)}}return a};djvu_html5.__computePropValue=k;o.__gwt_activeModules[S].bindings=djvu_html5.__getPropMap;r(O,Zb);if(q()){return D($b)}var l;try{h([Tb],_b);h([Pb],ac);h([Mb],bc);h([Vb],cc);h([Rb],dc);l=f[k(Kb)];var m=l.indexOf(ec);if(m!=-1){g=parseInt(l.substring(m+W),fb);l=l.substring(V,m)}}catch(a){}djvu_html5.__softPermutationId=g;return D(l+fc)}
function G(){if(!o.__gwt_stylesLoaded){o.__gwt_stylesLoaded={}}r(gc,P);r(gc,hc)}
B();djvu_html5.__moduleBase=C();t[S].moduleBase=djvu_html5.__moduleBase;var H=F();if(o){var I=!!(o.location.protocol==ic||o.location.protocol==jc);o.__gwt_activeModules[S].canRedirect=I;function J(){var b=kc;try{o.sessionStorage.setItem(b,b);o.sessionStorage.removeItem(b);return true}catch(a){return false}}
if(I&&J()){var K=lc;var L=o.sessionStorage[K];if(!/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/.*$/.test(L)){if(L&&(window.console&&console.log)){console.log(mc+L)}L=bb}if(L&&!o[K]){o[K]=true;o[K+nc]=C();var M=p.createElement(jb);M.src=L;var N=p.getElementsByTagName(ob)[V];N.insertBefore(M,N.firstElementChild||N.children[V]);return false}}}G();r(O,hc);A(H);return true}
djvu_html5.succeeded=djvu_html5();

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

@ -0,0 +1,2 @@
self.$wnd=self;self.$doc=self;function djvu_worker(){var c='81EB501BD7AB47786C30D3175CE1EA2B',d='.cache.js',e='djvu_worker',f='';var b;try{b=c}catch(a){return}importScripts(b+d);gwtOnLoad(undefined,e,f)}
djvu_worker();

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -87,7 +87,7 @@ $(".container-fluid").bind('drop', function (e) {
var files = e.originalEvent.dataTransfer.files;
var test = $("#btn-upload")[0].accept;
$(this).css('background', '');
const dt = new DataTransfer()
const dt = new DataTransfer();
jQuery.each(files, function (index, item) {
if (test.indexOf(item.name.substr(item.name.lastIndexOf('.'))) !== -1) {
dt.items.add(item);

@ -41,7 +41,7 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
else:
exc_command = [x for x in command]
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env)
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec
def process_wait(command, serr=subprocess.PIPE):

@ -55,6 +55,7 @@
{% endif %}
{% if reader_list and g.user.role_viewer() %}
<div class="btn-group" role="group">
{% if reader_list|length > 1 %}
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in Browser')}}
<span class="caret"></span>
@ -64,15 +65,19 @@
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
</ul>
{% else %}
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=reader_list[0])}}" id="readbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-eye-open"></span> {{_('Read in Browser')}} - {{reader_list[0]}}</a>
{% endif %}
</div>
{% endif %}
{% if audioentries|length > 0 and g.user.role_viewer() %}
<div class="btn-group" role="group">
{% if audioentries|length > 1 %}
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
{% for format in reader_list %}
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
@ -85,6 +90,9 @@
{% endif %}
{% endfor %}
</ul>
{% else %}
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=audioentries[0])}}" id="listenbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}} - {{audioentries[0]}}</a>
{% endif %}
</div>
{% endif %}
</div>

@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/libs/djvu_html5/Djvu_html5.css') }}">
<title>Djvu HTML5 browser demo</title>
<script type="text/javascript" language="javascript"
src="{{ url_for('static', filename='js/libs/djvu_html5/djvu_html5/djvu_html5.nocache.js') }}"></script>
</head>
<body>
<!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
<noscript>
<div
style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
Your web browser must have JavaScript enabled in order for this
application to display correctly.</div>
</noscript>
<div id="djvuContainer" file="{{ url_for('web.serve_book', book_id=djvufile,book_format='djvu') }}"></div>
<script type="text/javascript">
var DJVU_CONTEXT = {
background: "#666",
uiHideDelay: 1500,
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -284,6 +284,64 @@ class Updater(threading.Thread):
def _stable_version_info(cls):
return constants.STABLE_VERSION # Current version
@staticmethod
def _populate_parent_commits(update_data, status, locale, tz, parents):
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except (IndexError, KeyError):
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers, timeout=10)
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=locale)
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n', '<p>').replace('\n', '<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
return parents
@staticmethod
def _load_nightly_data(repository_url, commit, status):
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'],
headers=headers,
timeout=10)
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['message'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except (requests.exceptions.RequestException, ValueError):
status['message'] = _(u'General error')
return status, update_data
def _nightly_available_updates(self, request_method, locale):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request_method == "GET":
@ -309,22 +367,7 @@ class Updater(threading.Thread):
# a new update is available
status['update'] = True
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'],
headers=headers,
timeout=10)
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['message'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except (requests.exceptions.RequestException, ValueError):
status['message'] = _(u'General error')
status, update_data = self._load_nightly_data(repository_url, commit, status)
if status['message'] != '':
return json.dumps(status)
@ -346,41 +389,7 @@ class Updater(threading.Thread):
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except (IndexError, KeyError):
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
headers = {'Accept': 'application/vnd.github.v3+json'}
r = requests.get(parent_commit['url'], headers=headers, timeout=10)
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=locale)
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n', '<p>').replace('\n', '<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
parents = self._populate_parent_commits(update_data, status, locale, tz, parents)
status['history'] = parents[::-1]
except (IndexError, KeyError):
status['success'] = False

@ -191,7 +191,7 @@ def upload(uploadfile, rarExcecutable):
filename = uploadfile.filename
filename_root, file_extension = os.path.splitext(filename)
md5 = hashlib.md5(filename.encode('utf-8')).hexdigest()
md5 = hashlib.md5(filename.encode('utf-8')).hexdigest() # nosec
tmp_file_path = os.path.join(tmp_dir, md5)
log.debug("Temporary file: %s", tmp_file_path)
uploadfile.save(tmp_file_path)

@ -1632,6 +1632,9 @@ def read_book(book_id, book_format):
elif book_format.lower() == "txt":
log.debug(u"Start txt reader for %d", book_id)
return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book"))
elif book_format.lower() == "djvu":
log.debug(u"Start djvu reader for %d", book_id)
return render_title_template('readdjvu.html', djvufile=book_id, title=_(u"Read a Book"))
else:
for fileExt in constants.EXTENSIONS_AUDIO:
if book_format.lower() == fileExt:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save