Merge branch 'master' into Develop
This commit is contained in:
+2
-1
@@ -349,8 +349,9 @@ def list_users():
|
|||||||
if sort not in ub.User.__table__.columns.keys():
|
if sort not in ub.User.__table__.columns.keys():
|
||||||
sort = "id"
|
sort = "id"
|
||||||
order = request.args.get("order", "").lower()
|
order = request.args.get("order", "").lower()
|
||||||
|
|
||||||
if sort != "state" and order:
|
if sort != "state" and order:
|
||||||
|
if not order in ["asc", "desc"]:
|
||||||
|
order = "asc"
|
||||||
order = text(sort + " " + order)
|
order = text(sort + " " + order)
|
||||||
elif not state:
|
elif not state:
|
||||||
order = ub.User.id.asc()
|
order = ub.User.id.asc()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from cps.pagination import Pagination
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
|||||||
+4
-1
@@ -326,7 +326,9 @@ class ConfigSQL(object):
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
storage = {}
|
storage = {}
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
if k[0] != '_' and not k.endswith("_e") and not k == "cli" and 'api' not in k.lower():
|
if k[0] != '_' and not k.endswith("_e") and not k == "cli" \
|
||||||
|
and 'api' not in k.lower() and 'token' not in k.lower() \
|
||||||
|
and 'secret' not in k.lower():
|
||||||
storage[k] = v
|
storage[k] = v
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
@@ -583,6 +585,7 @@ def get_encryption_key(key_path):
|
|||||||
try:
|
try:
|
||||||
with open(key_file, "wb") as f:
|
with open(key_file, "wb") as f:
|
||||||
f.write(key)
|
f.write(key)
|
||||||
|
os.chmod(key_file, 0o600)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
error = e
|
error = e
|
||||||
return key, error
|
return key, error
|
||||||
|
|||||||
@@ -449,7 +449,14 @@ class Books(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def atom_timestamp(self):
|
def atom_timestamp(self):
|
||||||
return self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or ''
|
# OPDS atom:updated is defined as "the most recent instant in time
|
||||||
|
# when the entry was modified". Books.timestamp is the date added and
|
||||||
|
# never changes after import, so metadata and cover edits were
|
||||||
|
# invisible to OPDS sync clients. Use last_modified, which Calibre
|
||||||
|
# updates on every metadata or cover change; fall back to timestamp
|
||||||
|
# only if last_modified happens to be missing.
|
||||||
|
t = self.last_modified or self.timestamp
|
||||||
|
return t.strftime('%Y-%m-%dT%H:%M:%S+00:00') if t else ''
|
||||||
|
|
||||||
|
|
||||||
class CustomColumns(Base):
|
class CustomColumns(Base):
|
||||||
@@ -640,8 +647,8 @@ class CalibreDB:
|
|||||||
connect_args={'check_same_thread': False},
|
connect_args={'check_same_thread': False},
|
||||||
poolclass=StaticPool)
|
poolclass=StaticPool)
|
||||||
with check_engine.begin() as connection:
|
with check_engine.begin() as connection:
|
||||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||||
local_session = scoped_session(sessionmaker())
|
local_session = scoped_session(sessionmaker())
|
||||||
local_session.configure(bind=connection)
|
local_session.configure(bind=connection)
|
||||||
database_uuid = local_session().query(Library_Id).one_or_none()
|
database_uuid = local_session().query(Library_Id).one_or_none()
|
||||||
@@ -694,8 +701,8 @@ class CalibreDB:
|
|||||||
poolclass=StaticPool)
|
poolclass=StaticPool)
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text('PRAGMA cache_size = 10000;'))
|
connection.execute(text('PRAGMA cache_size = 10000;'))
|
||||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||||
|
|
||||||
conn = engine.connect()
|
conn = engine.connect()
|
||||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||||
@@ -1108,7 +1115,7 @@ class CalibreDB:
|
|||||||
.group_by(text('books_languages_link.lang_code')).all()
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
tags = list()
|
tags = list()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
tag = Category(isoLanguages.get_language_name(get_locale(), None, lang[0].lang_code), lang[0].lang_code)
|
tag = Category(isoLanguages.get_language_name(get_locale(), lang[0].lang_code), lang[0].lang_code)
|
||||||
tags.append([tag, lang[1]])
|
tags.append([tag, lang[1]])
|
||||||
# Append all books without language to list
|
# Append all books without language to list
|
||||||
if not return_all_languages:
|
if not return_all_languages:
|
||||||
|
|||||||
+2
-1
@@ -36,7 +36,8 @@ def do_calibre_export(book_id, book_format):
|
|||||||
if config.config_calibre_split:
|
if config.config_calibre_split:
|
||||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
library_path = config.get_book_path()
|
library_path = config.get_book_path()
|
||||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--dont-save-cover',
|
||||||
|
'--with-library', library_path,
|
||||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||||
str(book_id)]
|
str(book_id)]
|
||||||
p = process_open(opf_command, quotes, my_env)
|
p = process_open(opf_command, quotes, my_env)
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
|||||||
for cs in cover_section:
|
for cs in cover_section:
|
||||||
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
||||||
markup = epub_zip.read(os.path.join(cover_path, cs))
|
markup = epub_zip.read(os.path.join(cover_path, cs))
|
||||||
markup_tree = etree.fromstring(markup)
|
markup_tree = etree.fromstring(markup, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||||
# no matter xhtml or html with no namespace
|
# no matter xhtml or html with no namespace
|
||||||
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
|
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
|
||||||
# Alternative image source
|
# Alternative image source
|
||||||
|
|||||||
+6
-2
@@ -53,16 +53,20 @@ def updateEpub(src, dest, filename, data, ):
|
|||||||
zf.writestr(filename, data)
|
zf.writestr(filename, data)
|
||||||
|
|
||||||
|
|
||||||
|
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||||
|
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||||
|
|
||||||
|
|
||||||
def get_content_opf(file_path, ns=None):
|
def get_content_opf(file_path, ns=None):
|
||||||
if ns is None:
|
if ns is None:
|
||||||
ns = default_ns
|
ns = default_ns
|
||||||
epubZip = zipfile.ZipFile(file_path)
|
epubZip = zipfile.ZipFile(file_path)
|
||||||
txt = epubZip.read('META-INF/container.xml')
|
txt = epubZip.read('META-INF/container.xml')
|
||||||
tree = etree.fromstring(txt)
|
tree = etree.fromstring(txt, parser=_safe_parser)
|
||||||
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
||||||
cf = epubZip.read(cf_name)
|
cf = epubZip.read(cf_name)
|
||||||
|
|
||||||
return etree.fromstring(cf), cf_name
|
return etree.fromstring(cf, parser=_safe_parser), cf_name
|
||||||
|
|
||||||
|
|
||||||
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
||||||
|
|||||||
+10
-1
@@ -22,6 +22,8 @@ from flask import render_template, request, flash, make_response
|
|||||||
from flask_limiter import RateLimitExceeded
|
from flask_limiter import RateLimitExceeded
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from werkzeug.exceptions import default_exceptions
|
from werkzeug.exceptions import default_exceptions
|
||||||
|
|
||||||
|
from .cw_login import current_user
|
||||||
try:
|
try:
|
||||||
from werkzeug.exceptions import FailedDependency
|
from werkzeug.exceptions import FailedDependency
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -62,6 +64,13 @@ def internal_error(error):
|
|||||||
error_stack="",
|
error_stack="",
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), 500
|
), 500
|
||||||
|
log.error("500 Internal Server Error: %s", traceback.format_exc())
|
||||||
|
error_stack = ""
|
||||||
|
try:
|
||||||
|
if current_user.is_authenticated and current_user.role_admin():
|
||||||
|
error_stack = traceback.format_exc().split("\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return render_template('http_error.html',
|
return render_template('http_error.html',
|
||||||
error_code="500 Internal Server Error",
|
error_code="500 Internal Server Error",
|
||||||
error_name='The server encountered an internal error and was unable to complete your '
|
error_name='The server encountered an internal error and was unable to complete your '
|
||||||
@@ -69,7 +78,7 @@ def internal_error(error):
|
|||||||
issue=True,
|
issue=True,
|
||||||
goto_admin=False,
|
goto_admin=False,
|
||||||
unconfigured=False,
|
unconfigured=False,
|
||||||
error_stack=traceback.format_exc().split("\n"),
|
error_stack=error_stack,
|
||||||
instance=config.config_calibre_web_title
|
instance=config.config_calibre_web_title
|
||||||
), 500
|
), 500
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -20,6 +20,9 @@ from lxml import etree
|
|||||||
|
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
|
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||||
|
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||||
|
|
||||||
|
|
||||||
def get_fb2_info(tmp_file_path, original_file_extension):
|
def get_fb2_info(tmp_file_path, original_file_extension):
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
}
|
}
|
||||||
|
|
||||||
fb2_file = open(tmp_file_path, encoding="utf-8")
|
fb2_file = open(tmp_file_path, encoding="utf-8")
|
||||||
tree = etree.fromstring(fb2_file.read().encode())
|
tree = etree.fromstring(fb2_file.read().encode(), parser=_safe_parser)
|
||||||
|
|
||||||
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ try:
|
|||||||
if response:
|
if response:
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath).hexdigest(): # nosec
|
||||||
tmp_dir = get_temp_dir()
|
tmp_dir = get_temp_dir()
|
||||||
|
|
||||||
log.info('Database file updated')
|
log.info('Database file updated')
|
||||||
|
|||||||
+12
-1
@@ -30,7 +30,7 @@ import requests
|
|||||||
import unidecode
|
import unidecode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import send_from_directory, make_response, abort, url_for, Response, request
|
from flask import send_from_directory, make_response, abort, url_for, Response, request, after_this_request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
@@ -956,6 +956,17 @@ def do_download_file(book, book_format, client, data, headers):
|
|||||||
else:
|
else:
|
||||||
download_name = book_name
|
download_name = book_name
|
||||||
|
|
||||||
|
# Clean up staged copies in /tmp/calibre_web after the response is sent
|
||||||
|
# (kepubify / calibre-export branches) so the temp dir does not grow unbounded.
|
||||||
|
if filename == get_temp_dir():
|
||||||
|
_tmp_path = os.path.join(filename, download_name + "." + book_format)
|
||||||
|
@after_this_request
|
||||||
|
def _cleanup_staged_download(resp):
|
||||||
|
try:
|
||||||
|
os.remove(_tmp_path)
|
||||||
|
except OSError as ex:
|
||||||
|
log.warning('Failed to remove staged download %s: %s', _tmp_path, ex)
|
||||||
|
return resp
|
||||||
response = make_response(send_from_directory(filename, download_name + "." + book_format))
|
response = make_response(send_from_directory(filename, download_name + "." + book_format))
|
||||||
# ToDo Check headers parameter
|
# ToDo Check headers parameter
|
||||||
for element in headers:
|
for element in headers:
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
|||||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def generate_auth_token(user_id):
|
def generate_auth_token(user_id):
|
||||||
|
if current_user.id != user_id and not current_user.role_admin():
|
||||||
|
abort(403)
|
||||||
warning = False
|
warning = False
|
||||||
host_list = request.host.rsplit(':')
|
host_list = request.host.rsplit(':')
|
||||||
if len(host_list) == 1:
|
if len(host_list) == 1:
|
||||||
@@ -123,6 +125,8 @@ def generate_auth_token(user_id):
|
|||||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||||
@user_login_required
|
@user_login_required
|
||||||
def delete_auth_token(user_id):
|
def delete_auth_token(user_id):
|
||||||
|
if current_user.id != user_id and not current_user.role_admin():
|
||||||
|
abort(403)
|
||||||
# Invalidate any previously generated Kobo Auth token for this user
|
# Invalidate any previously generated Kobo Auth token for this user
|
||||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ComicVine(Metadata):
|
|||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
for result in result.json()["results"]:
|
for result in result.json()["results"]:
|
||||||
match = self._parse_search_result(
|
match = self._parse_search_result(
|
||||||
result=result, generic_cover=generic_cover, locale=locale
|
result=result, generic_cover=generic_cover, locale=locale
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class Douban(Metadata):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
|
|
||||||
match = MetaRecord(
|
match = MetaRecord(
|
||||||
id=id,
|
id=id,
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
|||||||
oauth_entry = query.first()
|
oauth_entry = query.first()
|
||||||
# already bind with user, just login
|
# already bind with user, just login
|
||||||
if oauth_entry.user:
|
if oauth_entry.user:
|
||||||
|
# If a user is already logged in and it's a different account, reject the link
|
||||||
|
# to prevent account takeover via shared OAuth identities
|
||||||
|
if current_user and current_user.is_authenticated and oauth_entry.user_id != current_user.id:
|
||||||
|
flash(_("This %(oauth)s account is already linked to a different user",
|
||||||
|
oauth=provider_name), category="error")
|
||||||
|
log.warning("User %s tried to link OAuth account already bound to user %s",
|
||||||
|
current_user.id, oauth_entry.user_id)
|
||||||
|
return redirect(url_for('web.profile'))
|
||||||
login_user(oauth_entry.user)
|
login_user(oauth_entry.user)
|
||||||
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
||||||
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
|
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class my_GoodreadsRequest(GoodreadsRequest):
|
|||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise GoodreadsRequestException(resp.reason, self.path)
|
raise GoodreadsRequestException(resp.reason, self.path)
|
||||||
if self.req_format == 'xml':
|
if self.req_format == 'xml':
|
||||||
root = etree.fromstring(resp.content)
|
root = etree.fromstring(resp.content, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||||
data_dict = etree_to_dict(root)
|
data_dict = etree_to_dict(root)
|
||||||
|
|
||||||
return data_dict['GoodreadsResponse']
|
return data_dict['GoodreadsResponse']
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ except ImportError:
|
|||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_ldap_filter(s):
|
||||||
|
"""Escape special characters for safe use in LDAP filter strings (RFC 4515)."""
|
||||||
|
s = s.replace('\\', '\\5c')
|
||||||
|
s = s.replace('*', '\\2a')
|
||||||
|
s = s.replace('(', '\\28')
|
||||||
|
s = s.replace(')', '\\29')
|
||||||
|
s = s.replace('\x00', '\\00')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class LDAPLogger(object):
|
class LDAPLogger(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -148,9 +158,11 @@ def bind_user(username, password):
|
|||||||
|
|
||||||
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
||||||
'''
|
'''
|
||||||
|
# Escape LDAP special characters to prevent LDAP injection in search filters
|
||||||
|
safe_username = _escape_ldap_filter(username)
|
||||||
try:
|
try:
|
||||||
if _ldap.get_object_details(username):
|
if _ldap.get_object_details(safe_username):
|
||||||
result = _ldap.bind_user(username, password)
|
result = _ldap.bind_user(safe_username, password)
|
||||||
log.debug("LDAP login '%s': %r", username, result)
|
log.debug("LDAP login '%s': %r", username, result)
|
||||||
return result is not None, None
|
return result is not None, None
|
||||||
return None, None # User not found
|
return None, None # User not found
|
||||||
|
|||||||
@@ -308,6 +308,9 @@ def order_shelf(shelf_id):
|
|||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
|
||||||
ub.BookShelf.order.asc()).all()
|
ub.BookShelf.order.asc()).all()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<h2>Details</h2>
|
<h2>Details</h2>
|
||||||
|
|
||||||
{% if entry.series|length > 0 %}
|
{% if entry.series|length > 0 %}
|
||||||
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(entry.series[0].name)|safe) }}</p>
|
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index|formatfloat(2), range=entry.series[0].name|e) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if entry.languages|length > 0 %}
|
{% if entry.languages|length > 0 %}
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -842,7 +842,8 @@ def list_books():
|
|||||||
order = request.args.get("order", "").lower()
|
order = request.args.get("order", "").lower()
|
||||||
state = None
|
state = None
|
||||||
join = tuple()
|
join = tuple()
|
||||||
|
if not order in ["asc", "desc", ""]:
|
||||||
|
order = "asc"
|
||||||
if sort_param == "state":
|
if sort_param == "state":
|
||||||
state = json.loads(request.args.get("state", "[]"))
|
state = json.loads(request.args.get("state", "[]"))
|
||||||
elif sort_param == "tags":
|
elif sort_param == "tags":
|
||||||
@@ -1194,7 +1195,9 @@ def get_robots():
|
|||||||
@viewer_required
|
@viewer_required
|
||||||
def serve_book(book_id, book_format, anyname):
|
def serve_book(book_id, book_format, anyname):
|
||||||
book_format = book_format.split(".")[0]
|
book_format = book_format.split(".")[0]
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
|
if not book:
|
||||||
|
return "File not in Database"
|
||||||
data = calibre_db.get_book_format(book_id, book_format.upper())
|
data = calibre_db.get_book_format(book_id, book_format.upper())
|
||||||
if not data:
|
if not data:
|
||||||
return "File not in Database"
|
return "File not in Database"
|
||||||
|
|||||||
+214
-209
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -33,12 +33,12 @@ dependencies = [
|
|||||||
"Flask>=1.0.2,<3.2.0",
|
"Flask>=1.0.2,<3.2.0",
|
||||||
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
||||||
"pycountry>=20.0.0,<27.0.0;python_version>='3.12'",
|
"pycountry>=20.0.0,<27.0.0;python_version>='3.12'",
|
||||||
"PyPDF>=6.1.3,<6.8.0",
|
"PyPDF>=6.1.3,<6.11.0",
|
||||||
"pytz>=2016.10",
|
"pytz>=2016.10",
|
||||||
"requests>=2.32.0,<2.33.0",
|
"requests>=2.32.0,<2.33.0",
|
||||||
"SQLAlchemy>=1.3.0,<2.1.0",
|
"SQLAlchemy>=1.3.0,<2.1.0",
|
||||||
"tornado>=6.4.2,<6.6",
|
"tornado>=6.4.2,<6.6",
|
||||||
"Wand>=0.4.4,<0.7.0",
|
"Wand>=0.4.4,<0.8.0",
|
||||||
"unidecode>=0.04.19,<1.5.0",
|
"unidecode>=0.04.19,<1.5.0",
|
||||||
"lxml>=4.9.1,<5.4.0",
|
"lxml>=4.9.1,<5.4.0",
|
||||||
"flask-wtf>=0.14.2,<1.3.0",
|
"flask-wtf>=0.14.2,<1.3.0",
|
||||||
@@ -51,7 +51,7 @@ dependencies = [
|
|||||||
"python-magic>=0.4.27,<0.5.0",
|
"python-magic>=0.4.27,<0.5.0",
|
||||||
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
||||||
"flask-httpAuth>=4.4.0,<5.0.0",
|
"flask-httpAuth>=4.4.0,<5.0.0",
|
||||||
"cryptography>=39.0.0,<47.0.0",
|
"cryptography>=39.0.0,<48.0.0",
|
||||||
"certifi>=2024.7.4,<2026.1.5",
|
"certifi>=2024.7.4,<2026.1.5",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|||||||
+3
-3
@@ -5,12 +5,12 @@ Flask-Principal>=0.3.2,<0.5.1
|
|||||||
Flask>=1.0.2,<3.2.0
|
Flask>=1.0.2,<3.2.0
|
||||||
iso-639>=0.4.5,<0.5.0;python_version<'3.12'
|
iso-639>=0.4.5,<0.5.0;python_version<'3.12'
|
||||||
pycountry>=20.0.0,<27.0.0;python_version>='3.12'
|
pycountry>=20.0.0,<27.0.0;python_version>='3.12'
|
||||||
PyPDF>=6.1.3,<6.8.0
|
PyPDF>=6.1.3,<6.11.0
|
||||||
pytz>=2016.10
|
pytz>=2016.10
|
||||||
requests>=2.32.0,<2.33.0
|
requests>=2.32.0,<2.33.0
|
||||||
SQLAlchemy>=1.3.0,<2.1.0
|
SQLAlchemy>=1.3.0,<2.1.0
|
||||||
tornado>=6.4.2,<6.6
|
tornado>=6.4.2,<6.6
|
||||||
Wand>=0.4.4,<0.7.0
|
Wand>=0.4.4,<0.8.0
|
||||||
unidecode>=0.04.19,<1.5.0
|
unidecode>=0.04.19,<1.5.0
|
||||||
lxml>=4.9.1,<5.4.0
|
lxml>=4.9.1,<5.4.0
|
||||||
flask-wtf>=0.14.2,<1.3.0
|
flask-wtf>=0.14.2,<1.3.0
|
||||||
@@ -23,5 +23,5 @@ bleach>=6.0.0,<6.4.0
|
|||||||
python-magic>=0.4.27,<0.5.0
|
python-magic>=0.4.27,<0.5.0
|
||||||
python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'
|
python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'
|
||||||
flask-httpAuth>=4.4.0,<5.0.0
|
flask-httpAuth>=4.4.0,<5.0.0
|
||||||
cryptography>=39.0.0,<47.0.0
|
cryptography>=39.0.0,<48.0.0
|
||||||
certifi>=2024.7.4,<2026.1.5
|
certifi>=2024.7.4,<2026.1.5
|
||||||
|
|||||||
+2212
-294
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user