Merge branch 'Develop'
- Back function for delete and edit books - configure ratelimiter backend possible - embed metadata during send to ereader - bugfixes split library - updated requirements
This commit is contained in:
commit
c30460d76b
14
cps/__init__.py
Normal file → Executable file
14
cps/__init__.py
Normal file → Executable file
|
@ -103,7 +103,7 @@ web_server = WebServer()
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
|
||||||
if limiter_present:
|
if limiter_present:
|
||||||
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
|
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=False)
|
||||||
else:
|
else:
|
||||||
limiter = None
|
limiter = None
|
||||||
|
|
||||||
|
@ -196,8 +196,18 @@ def create_app():
|
||||||
config.config_use_goodreads)
|
config.config_use_goodreads)
|
||||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
||||||
# Configure rate limiter
|
# Configure rate limiter
|
||||||
|
# https://limits.readthedocs.io/en/stable/storage.html
|
||||||
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
|
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
|
||||||
limiter.init_app(app)
|
if config.config_limiter_uri != "" and not cli_param.memory_backend:
|
||||||
|
app.config.update(RATELIMIT_STORAGE_URI=config.config_limiter_uri)
|
||||||
|
if config.config_limiter_options != "":
|
||||||
|
app.config.update(RATELIMIT_STORAGE_OPTIONS=config.config_limiter_options)
|
||||||
|
try:
|
||||||
|
limiter.init_app(app)
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Wrong Flask Limiter configuration, falling back to default: {}'.format(e))
|
||||||
|
app.config.update(RATELIMIT_STORAGE_URI=None)
|
||||||
|
limiter.init_app(app)
|
||||||
|
|
||||||
# Register scheduled tasks
|
# Register scheduled tasks
|
||||||
from .schedule import register_scheduled_tasks, register_startup_tasks
|
from .schedule import register_scheduled_tasks, register_startup_tasks
|
||||||
|
|
7
cps/admin.py
Normal file → Executable file
7
cps/admin.py
Normal file → Executable file
|
@ -47,7 +47,8 @@ from . import constants, logger, helper, services, cli_param
|
||||||
from . import db, calibre_db, ub, web_server, config, updater_thread, gdriveutils, \
|
from . import db, calibre_db, ub, web_server, config, updater_thread, gdriveutils, \
|
||||||
kobo_sync_status, schedule
|
kobo_sync_status, schedule
|
||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
||||||
valid_email, check_username, get_calibre_binarypath
|
valid_email, check_username
|
||||||
|
from .embed_helper import get_calibre_binarypath
|
||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .render_template import render_title_template, get_sidebar_config
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
|
@ -1716,7 +1717,7 @@ def _db_configuration_update_helper():
|
||||||
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
||||||
|
|
||||||
if db_change or not db_valid or not config.db_configured \
|
if db_change or not db_valid or not config.db_configured \
|
||||||
or config.config_calibre_dir != to_save["config_calibre_dir"]:
|
or config.config_calibre_dir != to_save["config_calibre_dir"]:
|
||||||
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
|
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
|
||||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||||
else:
|
else:
|
||||||
|
@ -1840,6 +1841,8 @@ def _configuration_update_helper():
|
||||||
return _configuration_result(_('Password length has to be between 1 and 40'))
|
return _configuration_result(_('Password length has to be between 1 and 40'))
|
||||||
reboot_required |= _config_int(to_save, "config_session")
|
reboot_required |= _config_int(to_save, "config_session")
|
||||||
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
|
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
|
||||||
|
reboot_required |= _config_string(to_save, "config_limiter_uri")
|
||||||
|
reboot_required |= _config_string(to_save, "config_limiter_options")
|
||||||
|
|
||||||
# Rarfile Content configuration
|
# Rarfile Content configuration
|
||||||
_config_string(to_save, "config_rarfile_location")
|
_config_string(to_save, "config_rarfile_location")
|
||||||
|
|
|
@ -52,6 +52,7 @@ class CliParameter(object):
|
||||||
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||||
|
parser.add_argument('-m', action='store_true', help='Use Memory-backend as limiter backend, use this parameter in case of miss configured backend')
|
||||||
parser.add_argument('-s', metavar='user:pass',
|
parser.add_argument('-s', metavar='user:pass',
|
||||||
help='Sets specific username to new password and exits Calibre-Web')
|
help='Sets specific username to new password and exits Calibre-Web')
|
||||||
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
||||||
|
@ -98,6 +99,8 @@ class CliParameter(object):
|
||||||
if args.k == "":
|
if args.k == "":
|
||||||
self.keyfilepath = ""
|
self.keyfilepath = ""
|
||||||
|
|
||||||
|
# overwrite limiter backend
|
||||||
|
self.memory_backend = args.m or None
|
||||||
# dry run updater
|
# dry run updater
|
||||||
self.dry_run = args.d or None
|
self.dry_run = args.d or None
|
||||||
# enable reconnect endpoint for docker database reconnect
|
# enable reconnect endpoint for docker database reconnect
|
||||||
|
|
|
@ -168,6 +168,8 @@ class _Settings(_Base):
|
||||||
config_password_special = Column(Boolean, default=True)
|
config_password_special = Column(Boolean, default=True)
|
||||||
config_session = Column(Integer, default=1)
|
config_session = Column(Integer, default=1)
|
||||||
config_ratelimiter = Column(Boolean, default=True)
|
config_ratelimiter = Column(Boolean, default=True)
|
||||||
|
config_limiter_uri = Column(String, default="")
|
||||||
|
config_limiter_options = Column(String, default="")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
|
@ -60,6 +60,7 @@ from .tasks.upload import TaskUpload
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano
|
||||||
from .kobo_sync_status import change_archived_books
|
from .kobo_sync_status import change_archived_books
|
||||||
|
from .redirect import get_redirect_location
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('edit-book', __name__)
|
editbook = Blueprint('edit-book', __name__)
|
||||||
|
@ -96,7 +97,7 @@ def delete_book_from_details(book_id):
|
||||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book_ajax(book_id, book_format):
|
||||||
return delete_book_from_table(book_id, book_format, False)
|
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
|
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
|
||||||
|
@ -823,7 +824,7 @@ def delete_whole_book(book_id, book):
|
||||||
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
||||||
|
|
||||||
|
|
||||||
def render_delete_book_result(book_format, json_response, warning, book_id):
|
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
|
||||||
if book_format:
|
if book_format:
|
||||||
if json_response:
|
if json_response:
|
||||||
return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
|
@ -835,16 +836,16 @@ def render_delete_book_result(book_format, json_response, warning, book_id):
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
else:
|
else:
|
||||||
if json_response:
|
if json_response:
|
||||||
return json.dumps([warning, {"location": url_for('web.index'),
|
return json.dumps([warning, {"location": get_redirect_location(location, "web.index"),
|
||||||
"type": "success",
|
"type": "success",
|
||||||
"format": book_format,
|
"format": book_format,
|
||||||
"message": _('Book Successfully Deleted')}])
|
"message": _('Book Successfully Deleted')}])
|
||||||
else:
|
else:
|
||||||
flash(_('Book Successfully Deleted'), category="success")
|
flash(_('Book Successfully Deleted'), category="success")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(get_redirect_location(location, "web.index"))
|
||||||
|
|
||||||
|
|
||||||
def delete_book_from_table(book_id, book_format, json_response):
|
def delete_book_from_table(book_id, book_format, json_response, location=""):
|
||||||
warning = {}
|
warning = {}
|
||||||
if current_user.role_delete_books():
|
if current_user.role_delete_books():
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
|
@ -891,7 +892,7 @@ def delete_book_from_table(book_id, book_format, json_response):
|
||||||
else:
|
else:
|
||||||
# book not found
|
# book not found
|
||||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
return render_delete_book_result(book_format, json_response, warning, book_id)
|
return render_delete_book_result(book_format, json_response, warning, book_id, location)
|
||||||
message = _("You are missing permissions to delete books")
|
message = _("You are missing permissions to delete books")
|
||||||
if json_response:
|
if json_response:
|
||||||
return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
|
||||||
|
|
63
cps/embed_helper.py
Normal file
63
cps/embed_helper.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2024 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
from uuid import uuid4
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .file_helper import get_temp_dir
|
||||||
|
from .subproc_wrapper import process_open
|
||||||
|
from . import logger, config
|
||||||
|
from .constants import SUPPORTED_CALIBRE_BINARIES
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def do_calibre_export(book_id, book_format):
|
||||||
|
try:
|
||||||
|
quotes = [3, 5, 7, 9]
|
||||||
|
tmp_dir = get_temp_dir()
|
||||||
|
calibredb_binarypath = get_calibre_binarypath("calibredb")
|
||||||
|
temp_file_name = str(uuid4())
|
||||||
|
my_env = os.environ.copy()
|
||||||
|
if config.config_calibre_split:
|
||||||
|
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
|
library_path = config.config_calibre_split_dir
|
||||||
|
else:
|
||||||
|
library_path = config.config_calibre_dir
|
||||||
|
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
||||||
|
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||||
|
str(book_id)]
|
||||||
|
p = process_open(opf_command, quotes, my_env)
|
||||||
|
_, err = p.communicate()
|
||||||
|
if err:
|
||||||
|
log.error('Metadata embedder encountered an error: %s', err)
|
||||||
|
return tmp_dir, temp_file_name
|
||||||
|
except OSError as ex:
|
||||||
|
# ToDo real error handling
|
||||||
|
log.error_or_exception(ex)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_calibre_binarypath(binary):
|
||||||
|
binariesdir = config.config_binariesdir
|
||||||
|
if binariesdir:
|
||||||
|
try:
|
||||||
|
return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary])
|
||||||
|
except KeyError as ex:
|
||||||
|
log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary])
|
||||||
|
pass
|
||||||
|
return ""
|
|
@ -28,7 +28,6 @@ from datetime import datetime, timedelta
|
||||||
import requests
|
import requests
|
||||||
import unidecode
|
import unidecode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
@ -56,13 +55,14 @@ from .tasks.convert import TaskConvert
|
||||||
from . import logger, config, db, ub, fs
|
from . import logger, config, db, ub, fs
|
||||||
from . import gdriveutils as gd
|
from . import gdriveutils as gd
|
||||||
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES
|
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES
|
||||||
from .subproc_wrapper import process_wait, process_open
|
from .subproc_wrapper import process_wait
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.mail import TaskEmail
|
from .tasks.mail import TaskEmail
|
||||||
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
|
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
|
||||||
from .tasks.metadata_backup import TaskBackupMetadata
|
from .tasks.metadata_backup import TaskBackupMetadata
|
||||||
from .file_helper import get_temp_dir
|
from .file_helper import get_temp_dir
|
||||||
from .epub_helper import get_content_opf, create_new_metadata_backup, updateEpub, replace_metadata
|
from .epub_helper import get_content_opf, create_new_metadata_backup, updateEpub, replace_metadata
|
||||||
|
from .embed_helper import do_calibre_export
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id)
|
||||||
email_text = N_("%(book)s send to eReader", book=link)
|
email_text = N_("%(book)s send to eReader", book=link)
|
||||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), ereader_mail,
|
config.get_mail_settings(), ereader_mail,
|
||||||
email_text, _('This Email has been sent via Calibre-Web.')))
|
email_text, _('This Email has been sent via Calibre-Web.'),book.id))
|
||||||
return
|
return
|
||||||
return _("The requested file could not be read. Maybe wrong permissions?")
|
return _("The requested file could not be read. Maybe wrong permissions?")
|
||||||
|
|
||||||
|
@ -692,15 +692,15 @@ def valid_password(check_password):
|
||||||
if config.config_password_policy:
|
if config.config_password_policy:
|
||||||
verify = ""
|
verify = ""
|
||||||
if config.config_password_min_length > 0:
|
if config.config_password_min_length > 0:
|
||||||
verify += "^(?=.{" + str(config.config_password_min_length) + ",}$)"
|
verify += r"^(?=.{" + str(config.config_password_min_length) + ",}$)"
|
||||||
if config.config_password_number:
|
if config.config_password_number:
|
||||||
verify += "(?=.*?\d)"
|
verify += r"(?=.*?\d)"
|
||||||
if config.config_password_lower:
|
if config.config_password_lower:
|
||||||
verify += "(?=.*?[a-z])"
|
verify += r"(?=.*?[a-z])"
|
||||||
if config.config_password_upper:
|
if config.config_password_upper:
|
||||||
verify += "(?=.*?[A-Z])"
|
verify += r"(?=.*?[A-Z])"
|
||||||
if config.config_password_special:
|
if config.config_password_special:
|
||||||
verify += "(?=.*?[^A-Za-z\s0-9])"
|
verify += r"(?=.*?[^A-Za-z\s0-9])"
|
||||||
match = re.match(verify, check_password)
|
match = re.match(verify, check_password)
|
||||||
if not match:
|
if not match:
|
||||||
raise Exception(_("Password doesn't comply with password validation rules"))
|
raise Exception(_("Password doesn't comply with password validation rules"))
|
||||||
|
@ -1001,33 +1001,6 @@ def do_kepubify_metadata_replace(book, file_path):
|
||||||
return tmp_dir, temp_file_name
|
return tmp_dir, temp_file_name
|
||||||
|
|
||||||
|
|
||||||
def do_calibre_export(book_id, book_format, ):
|
|
||||||
try:
|
|
||||||
quotes = [3, 5, 7, 9]
|
|
||||||
tmp_dir = get_temp_dir()
|
|
||||||
calibredb_binarypath = get_calibre_binarypath("calibredb")
|
|
||||||
temp_file_name = str(uuid4())
|
|
||||||
my_env = os.environ.copy()
|
|
||||||
if config.config_calibre_split:
|
|
||||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
|
||||||
library_path = config.config_calibre_split_dir
|
|
||||||
else:
|
|
||||||
library_path = config.config_calibre_dir
|
|
||||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
|
||||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
|
||||||
str(book_id)]
|
|
||||||
# CALIBRE_OVERRIDE_DATABASE_PATH
|
|
||||||
p = process_open(opf_command, quotes, my_env)
|
|
||||||
_, err = p.communicate()
|
|
||||||
if err:
|
|
||||||
log.error('Metadata embedder encountered an error: %s', err)
|
|
||||||
return tmp_dir, temp_file_name
|
|
||||||
except OSError as ex:
|
|
||||||
# ToDo real error handling
|
|
||||||
log.error_or_exception(ex)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
|
|
||||||
|
@ -1066,7 +1039,7 @@ def check_calibre(calibre_location):
|
||||||
binaries_available = [os.path.isfile(binary_path) for binary_path in supported_binary_paths]
|
binaries_available = [os.path.isfile(binary_path) for binary_path in supported_binary_paths]
|
||||||
binaries_executable = [os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths]
|
binaries_executable = [os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths]
|
||||||
if all(binaries_available) and all(binaries_executable):
|
if all(binaries_available) and all(binaries_executable):
|
||||||
values = [process_wait([binary_path, "--version"], pattern='\(calibre (.*)\)')
|
values = [process_wait([binary_path, "--version"], pattern=r'\(calibre (.*)\)')
|
||||||
for binary_path in supported_binary_paths]
|
for binary_path in supported_binary_paths]
|
||||||
if all(values):
|
if all(values):
|
||||||
version = values[0].group(1)
|
version = values[0].group(1)
|
||||||
|
@ -1149,17 +1122,6 @@ def get_download_link(book_id, book_format, client):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
def get_calibre_binarypath(binary):
|
|
||||||
binariesdir = config.config_binariesdir
|
|
||||||
if binariesdir:
|
|
||||||
try:
|
|
||||||
return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary])
|
|
||||||
except KeyError as ex:
|
|
||||||
log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary])
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def clear_cover_thumbnail_cache(book_id):
|
def clear_cover_thumbnail_cache(book_id):
|
||||||
if config.schedule_generate_book_covers:
|
if config.schedule_generate_book_covers:
|
||||||
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True)
|
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True)
|
||||||
|
|
|
@ -156,6 +156,9 @@ def requires_kobo_auth(f):
|
||||||
limiter.check()
|
limiter.check()
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
return abort(429)
|
return abort(429)
|
||||||
|
except (ConnectionError, Exception) as e:
|
||||||
|
log.error("Connection error to limiter backend: %s", e)
|
||||||
|
return abort(429)
|
||||||
user = (
|
user = (
|
||||||
ub.session.query(ub.User)
|
ub.session.query(ub.User)
|
||||||
.join(ub.RemoteAuthToken)
|
.join(ub.RemoteAuthToken)
|
||||||
|
|
|
@ -44,9 +44,9 @@ def remove_prefix(text, prefix):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def redirect_back(endpoint, **values):
|
def get_redirect_location(next, endpoint, **values):
|
||||||
target = request.form.get('next', None) or url_for(endpoint, **values)
|
target = next or url_for(endpoint, **values)
|
||||||
adapter = current_app.url_map.bind(urlparse(request.host_url).netloc)
|
adapter = current_app.url_map.bind(urlparse(request.host_url).netloc)
|
||||||
if not len(adapter.allowed_methods(remove_prefix(target, request.environ.get('HTTP_X_SCRIPT_NAME',"")))):
|
if not len(adapter.allowed_methods(remove_prefix(target, request.environ.get('HTTP_X_SCRIPT_NAME',"")))):
|
||||||
target = url_for(endpoint, **values)
|
target = url_for(endpoint, **values)
|
||||||
return redirect(target)
|
return target
|
||||||
|
|
|
@ -266,3 +266,6 @@ class CalibreTask:
|
||||||
def _handleSuccess(self):
|
def _handleSuccess(self):
|
||||||
self.stat = STAT_FINISH_SUCCESS
|
self.stat = STAT_FINISH_SUCCESS
|
||||||
self.progress = 1
|
self.progress = 1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
|
@ -20,7 +20,7 @@ function getPath() {
|
||||||
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
|
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
|
||||||
}
|
}
|
||||||
|
|
||||||
function postButton(event, action){
|
function postButton(event, action, location=""){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var newForm = jQuery('<form>', {
|
var newForm = jQuery('<form>', {
|
||||||
"action": action,
|
"action": action,
|
||||||
|
@ -30,7 +30,14 @@ function postButton(event, action){
|
||||||
'name': 'csrf_token',
|
'name': 'csrf_token',
|
||||||
'value': $("input[name=\'csrf_token\']").val(),
|
'value': $("input[name=\'csrf_token\']").val(),
|
||||||
'type': 'hidden'
|
'type': 'hidden'
|
||||||
})).appendTo('body');
|
})).appendTo('body')
|
||||||
|
if(location !== "") {
|
||||||
|
newForm.append(jQuery('<input>', {
|
||||||
|
'name': 'location',
|
||||||
|
'value': location,
|
||||||
|
'type': 'hidden'
|
||||||
|
})).appendTo('body');
|
||||||
|
}
|
||||||
newForm.submit();
|
newForm.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,17 +219,20 @@ $("#delete_confirm").click(function(event) {
|
||||||
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
|
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
|
||||||
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#books-table").bootstrapTable("refresh");
|
$("#books-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
postButton(event, getPath() + "/delete/" + deleteId);
|
var loc = sessionStorage.getItem("back");
|
||||||
|
if (!loc) {
|
||||||
|
loc = $(this).data("back");
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem("back");
|
||||||
|
postButton(event, getPath() + "/delete/" + deleteId, location=loc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//triggered when modal is about to be shown
|
//triggered when modal is about to be shown
|
||||||
|
@ -541,6 +551,7 @@ $(function() {
|
||||||
$.get(e.relatedTarget.href).done(function(content) {
|
$.get(e.relatedTarget.href).done(function(content) {
|
||||||
$modalBody.html(content);
|
$modalBody.html(content);
|
||||||
preFilters.remove(useCache);
|
preFilters.remove(useCache);
|
||||||
|
$("#back").remove();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.on("hidden.bs.modal", function() {
|
.on("hidden.bs.modal", function() {
|
||||||
|
|
|
@ -110,6 +110,7 @@ class TaskConvert(CalibreTask):
|
||||||
self.ereader_mail,
|
self.ereader_mail,
|
||||||
EmailText,
|
EmailText,
|
||||||
self.settings['body'],
|
self.settings['body'],
|
||||||
|
id=self.book_id,
|
||||||
internal=True)
|
internal=True)
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
@ -315,9 +316,9 @@ class TaskConvert(CalibreTask):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.ereader_mail:
|
if self.ereader_mail:
|
||||||
return "Convert {} {}".format(self.book_id, self.ereader_mail)
|
return "Convert Book {} and mail it to {}".format(self.book_id, self.ereader_mail)
|
||||||
else:
|
else:
|
||||||
return "Convert {}".format(self.book_id)
|
return "Convert Book {}".format(self.book_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
def is_cancellable(self):
|
||||||
|
|
|
@ -28,12 +28,11 @@ from email.message import EmailMessage
|
||||||
from email.utils import formatdate, parseaddr
|
from email.utils import formatdate, parseaddr
|
||||||
from email.generator import Generator
|
from email.generator import Generator
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from email.utils import formatdate
|
|
||||||
|
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
from cps.services import gmail
|
from cps.services import gmail
|
||||||
|
from cps.embed_helper import do_calibre_export
|
||||||
from cps import logger, config
|
from cps import logger, config
|
||||||
|
|
||||||
from cps import gdriveutils
|
from cps import gdriveutils
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ class EmailSSL(EmailBase, smtplib.SMTP_SSL):
|
||||||
|
|
||||||
|
|
||||||
class TaskEmail(CalibreTask):
|
class TaskEmail(CalibreTask):
|
||||||
def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, internal=False):
|
def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, id=0, internal=False):
|
||||||
super(TaskEmail, self).__init__(task_message)
|
super(TaskEmail, self).__init__(task_message)
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
|
@ -119,6 +118,7 @@ class TaskEmail(CalibreTask):
|
||||||
self.recipient = recipient
|
self.recipient = recipient
|
||||||
self.text = text
|
self.text = text
|
||||||
self.asyncSMTP = None
|
self.asyncSMTP = None
|
||||||
|
self.book_id = id
|
||||||
self.results = dict()
|
self.results = dict()
|
||||||
|
|
||||||
# from calibre code:
|
# from calibre code:
|
||||||
|
@ -141,7 +141,7 @@ class TaskEmail(CalibreTask):
|
||||||
message['To'] = self.recipient
|
message['To'] = self.recipient
|
||||||
message['Subject'] = self.subject
|
message['Subject'] = self.subject
|
||||||
message['Date'] = formatdate(localtime=True)
|
message['Date'] = formatdate(localtime=True)
|
||||||
message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain()) # f"<{uuid.uuid4()}@{get_msgid_domain(from_)}>" # make_msgid('calibre-web')
|
message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain())
|
||||||
message.set_content(self.text.encode('UTF-8'), "text", "plain")
|
message.set_content(self.text.encode('UTF-8'), "text", "plain")
|
||||||
if self.attachment:
|
if self.attachment:
|
||||||
data = self._get_attachment(self.filepath, self.attachment)
|
data = self._get_attachment(self.filepath, self.attachment)
|
||||||
|
@ -161,6 +161,8 @@ class TaskEmail(CalibreTask):
|
||||||
try:
|
try:
|
||||||
# create MIME message
|
# create MIME message
|
||||||
msg = self.prepare_message()
|
msg = self.prepare_message()
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
if self.settings['mail_server_type'] == 0:
|
if self.settings['mail_server_type'] == 0:
|
||||||
self.send_standard_email(msg)
|
self.send_standard_email(msg)
|
||||||
else:
|
else:
|
||||||
|
@ -236,10 +238,10 @@ class TaskEmail(CalibreTask):
|
||||||
self.asyncSMTP = None
|
self.asyncSMTP = None
|
||||||
self._progress = x
|
self._progress = x
|
||||||
|
|
||||||
@classmethod
|
def _get_attachment(self, book_path, filename):
|
||||||
def _get_attachment(cls, book_path, filename):
|
|
||||||
"""Get file as MIMEBase message"""
|
"""Get file as MIMEBase message"""
|
||||||
calibre_path = config.get_book_path()
|
calibre_path = config.get_book_path()
|
||||||
|
extension = os.path.splitext(filename)[1][1:]
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
|
df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
|
||||||
if df:
|
if df:
|
||||||
|
@ -249,15 +251,22 @@ class TaskEmail(CalibreTask):
|
||||||
df.GetContentFile(datafile)
|
df.GetContentFile(datafile)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
file_ = open(datafile, 'rb')
|
if config.config_binariesdir and config.config_embed_metadata:
|
||||||
data = file_.read()
|
data_path, data_file = do_calibre_export(self.book_id, extension)
|
||||||
file_.close()
|
datafile = os.path.join(data_path, data_file + "." + extension)
|
||||||
|
with open(datafile, 'rb') as file_:
|
||||||
|
data = file_.read()
|
||||||
os.remove(datafile)
|
os.remove(datafile)
|
||||||
else:
|
else:
|
||||||
|
datafile = os.path.join(calibre_path, book_path, filename)
|
||||||
try:
|
try:
|
||||||
file_ = open(os.path.join(calibre_path, book_path, filename), 'rb')
|
if config.config_binariesdir and config.config_embed_metadata:
|
||||||
data = file_.read()
|
data_path, data_file = do_calibre_export(self.book_id, extension)
|
||||||
file_.close()
|
datafile = os.path.join(data_path, data_file + "." + extension)
|
||||||
|
with open(datafile, 'rb') as file_:
|
||||||
|
data = file_.read()
|
||||||
|
if config.config_binariesdir and config.config_embed_metadata:
|
||||||
|
os.remove(datafile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
log.error_or_exception(e, stacklevel=3)
|
log.error_or_exception(e, stacklevel=3)
|
||||||
log.error('The requested file could not be read. Maybe wrong permissions?')
|
log.error('The requested file could not be read. Maybe wrong permissions?')
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book session">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
||||||
<span class="img" title="{{entry.Books.title}}">
|
<span class="img" title="{{entry.Books.title}}">
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
<h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
|
<h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for entry in other_books %}
|
{% for entry in other_books %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
|
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
|
||||||
<img title="{{entry.title}}" src="{{ entry.image_url }}" />
|
<img title="{{entry.title}}" src="{{ entry.image_url }}" />
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group required">
|
<div class="form-group required">
|
||||||
<input type="checkbox" id="config_calibre_split" name="config_calibre_split" data-control="split_settings" data-t ="{{ config.config_calibre_split_dir }}" {% if config.config_calibre_split %}checked{% endif %} >
|
<input type="checkbox" id="config_calibre_split" name="config_calibre_split" data-control="split_settings" data-t ="{{ config.config_calibre_split_dir }}" {% if config.config_calibre_split %}checked{% endif %} >
|
||||||
<label for="config_calibre_split">{{_('Separate Book files from Library (Experimental, may lead to unexpected behavior)')}}</label>
|
<label for="config_calibre_split">{{_('Separate Book Files from Library')}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div data-related="split_settings">
|
<div data-related="split_settings">
|
||||||
<div class="form-group required input-group">
|
<div class="form-group required input-group">
|
||||||
|
|
12
cps/templates/config_edit.html
Normal file → Executable file
12
cps/templates/config_edit.html
Normal file → Executable file
|
@ -105,7 +105,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="config_embed_metadata" name="config_embed_metadata" {% if config.config_embed_metadata %}checked{% endif %}>
|
<input type="checkbox" id="config_embed_metadata" name="config_embed_metadata" {% if config.config_embed_metadata %}checked{% endif %}>
|
||||||
<label for="config_embed_metadata">{{_('Embed Metadata to Ebook File on Download and Conversion (needs Calibre/Kepubify binaries)')}}</label>
|
<label for="config_embed_metadata">{{_('Embed Metadata to Ebook File on Download/Conversion/e-mail (needs Calibre/Kepubify binaries)')}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
|
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
|
||||||
|
@ -372,6 +372,16 @@
|
||||||
<input type="checkbox" id="config_ratelimiter" name="config_ratelimiter" {% if config.config_ratelimiter %}checked{% endif %}>
|
<input type="checkbox" id="config_ratelimiter" name="config_ratelimiter" {% if config.config_ratelimiter %}checked{% endif %}>
|
||||||
<label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label>
|
<label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-related="ratelimiter_settings">
|
||||||
|
<div class="form-group" style="margin-left:10px;">
|
||||||
|
<label for="config_calibre">{{_('Configure Backend for Limiter')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_limiter_uri" name="config_limiter_uri" value="{% if config.config_limiter_uri != None %}{{ config.config_limiter_uri }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-left:10px;">
|
||||||
|
<label for="config_calibre">{{_('Options for Limiter')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_limiter_options" name="config_limiter_options" value="{% if config.config_limiter_options != None %}{{ config.config_limiter_options }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="config_session">{{_('Session protection')}}</label>
|
<label for="config_session">{{_('Session protection')}}</label>
|
||||||
<select name="config_session" id="config_session" class="form-control">
|
<select name="config_session" id="config_session" class="form-control">
|
||||||
|
|
7
cps/templates/detail.html
Executable file → Normal file
7
cps/templates/detail.html
Executable file → Normal file
|
@ -333,15 +333,15 @@
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role_edit() %}
|
{% if current_user.role_edit() %}
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="col-sm-12">
|
||||||
<div class="btn-group" role="group" aria-label="Edit/Delete book">
|
<div class="btn-group" role="group" aria-label="Edit/Delete book">
|
||||||
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}"
|
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}"
|
||||||
class="btn btn-sm btn-primary" id="edit_book" role="button"><span
|
class="btn btn-sm btn-primary" id="edit_book" role="button"><span
|
||||||
class="glyphicon glyphicon-edit"></span> {{ _('Edit Metadata') }}</a>
|
class="glyphicon glyphicon-edit"></span> {{ _('Edit Metadata') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="btn btn-default" data-back="{{ url_for('web.index') }}" id="back">{{_('Cancel')}}</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -367,4 +367,3 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
|
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
{% for entry in random %}
|
{% for entry in random %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book session" id="books_rand">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
||||||
<span class="img" title="{{ entry.Books.title }}">
|
<span class="img" title="{{ entry.Books.title }}">
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
{% if entries[0] %}
|
{% if entries[0] %}
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book session" id="books">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
||||||
<span class="img" title="{{ entry.Books.title }}">
|
<span class="img" title="{{ entry.Books.title }}">
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
{% if entry.Books.has_cover is defined %}
|
{% if entry.Books.has_cover is defined %}
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
|
||||||
<span class="img" title="{{entry.Books.title}}" >
|
<span class="img" title="{{entry.Books.title}}" >
|
||||||
|
|
14
cps/web.py
14
cps/web.py
|
@ -50,7 +50,7 @@ from .helper import check_valid_domain, check_email, check_username, \
|
||||||
send_registration_mail, check_send_to_ereader, check_read_formats, tags_filters, reset_password, valid_email, \
|
send_registration_mail, check_send_to_ereader, check_read_formats, tags_filters, reset_password, valid_email, \
|
||||||
edit_book_read_status, valid_password
|
edit_book_read_status, valid_password
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .redirect import redirect_back
|
from .redirect import get_redirect_location
|
||||||
from .babel import get_available_locale
|
from .babel import get_available_locale
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano
|
||||||
from .kobo_sync_status import remove_synced_book
|
from .kobo_sync_status import remove_synced_book
|
||||||
|
@ -1276,6 +1276,10 @@ def register_post():
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
flash(_(u"Please wait one minute to register next user"), category="error")
|
flash(_(u"Please wait one minute to register next user"), category="error")
|
||||||
return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||||
|
except (ConnectionError, Exception) as e:
|
||||||
|
log.error("Connection error to limiter backend: %s", e)
|
||||||
|
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
||||||
|
return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not config.get_mail_server_configured():
|
if not config.get_mail_server_configured():
|
||||||
|
@ -1338,7 +1342,7 @@ def handle_login_user(user, remember, message, category):
|
||||||
ub.store_user_session()
|
ub.store_user_session()
|
||||||
flash(message, category=category)
|
flash(message, category=category)
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
return redirect_back("web.index")
|
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||||
|
|
||||||
|
|
||||||
def render_login(username="", password=""):
|
def render_login(username="", password=""):
|
||||||
|
@ -1374,7 +1378,11 @@ def login_post():
|
||||||
try:
|
try:
|
||||||
limiter.check()
|
limiter.check()
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
flash(_(u"Please wait one minute before next login"), category="error")
|
flash(_("Please wait one minute before next login"), category="error")
|
||||||
|
return render_login(username, form.get("password", ""))
|
||||||
|
except (ConnectionError, Exception) as e:
|
||||||
|
log.error("Connection error to limiter backend: %s", e)
|
||||||
|
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
||||||
return render_login(username, form.get("password", ""))
|
return render_login(username, form.get("password", ""))
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
# GDrive Integration
|
# GDrive Integration
|
||||||
google-api-python-client>=1.7.11,<2.108.0
|
google-api-python-client>=1.7.11,<2.120.0
|
||||||
gevent>20.6.0,<24.0.0
|
gevent>20.6.0,<24.3.0
|
||||||
greenlet>=0.4.17,<3.1.0
|
greenlet>=0.4.17,<3.1.0
|
||||||
httplib2>=0.9.2,<0.23.0
|
httplib2>=0.9.2,<0.23.0
|
||||||
oauth2client>=4.0.0,<4.1.4
|
oauth2client>=4.0.0,<4.1.4
|
||||||
uritemplate>=3.0.0,<4.2.0
|
uritemplate>=3.0.0,<4.2.0
|
||||||
pyasn1-modules>=0.0.8,<0.4.0
|
pyasn1-modules>=0.0.8,<0.4.0
|
||||||
pyasn1>=0.1.9,<0.6.0
|
pyasn1>=0.1.9,<0.6.0
|
||||||
PyDrive2>=1.3.1,<1.18.0
|
PyDrive2>=1.3.1,<1.20.0
|
||||||
PyYAML>=3.12,<6.1
|
PyYAML>=3.12,<6.1
|
||||||
rsa>=3.4.2,<4.10.0
|
rsa>=3.4.2,<4.10.0
|
||||||
|
|
||||||
# Gmail
|
# Gmail
|
||||||
google-auth-oauthlib>=0.4.3,<1.1.0
|
google-auth-oauthlib>=0.4.3,<1.3.0
|
||||||
google-api-python-client>=1.7.11,<2.108.0
|
google-api-python-client>=1.7.11,<2.120.0
|
||||||
|
|
||||||
# goodreads
|
# goodreads
|
||||||
goodreads>=0.3.2,<0.4.0
|
goodreads>=0.3.2,<0.4.0
|
||||||
python-Levenshtein>=0.12.0,<0.22.0
|
python-Levenshtein>=0.12.0,<0.26.0
|
||||||
|
|
||||||
# ldap login
|
# ldap login
|
||||||
python-ldap>=3.0.0,<3.5.0
|
python-ldap>=3.0.0,<3.5.0
|
||||||
|
@ -28,10 +28,10 @@ Flask-Dance>=2.0.0,<7.1.0
|
||||||
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
||||||
|
|
||||||
# metadata extraction
|
# metadata extraction
|
||||||
rarfile>=3.2
|
rarfile>=3.2,<4.2
|
||||||
scholarly>=1.2.0,<1.8
|
scholarly>=1.2.0,<1.8
|
||||||
markdown2>=2.0.0,<2.5.0
|
markdown2>=2.0.0,<2.5.0
|
||||||
html2text>=2020.1.16,<2022.1.1
|
html2text>=2020.1.16,<2024.2.26
|
||||||
python-dateutil>=2.1,<2.9.0
|
python-dateutil>=2.1,<2.9.0
|
||||||
beautifulsoup4>=4.0.1,<4.13.0
|
beautifulsoup4>=4.0.1,<4.13.0
|
||||||
faust-cchardet>=2.1.18,<2.1.20
|
faust-cchardet>=2.1.18,<2.1.20
|
||||||
|
|
|
@ -2,18 +2,18 @@ Werkzeug<3.0.0
|
||||||
APScheduler>=3.6.3,<3.11.0
|
APScheduler>=3.6.3,<3.11.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<4.1.0
|
Flask-Babel>=0.11.1,<4.1.0
|
||||||
Flask-Login>=0.3.2,<0.6.3
|
Flask-Login>=0.3.2,<0.6.4
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
Flask>=1.0.2,<2.4.0
|
Flask>=1.0.2,<3.1.0
|
||||||
iso-639>=0.4.5,<0.5.0
|
iso-639>=0.4.5,<0.5.0
|
||||||
PyPDF>=3.0.0,<3.16.0
|
PyPDF>=3.15.6,<4.1.0
|
||||||
pytz>=2016.10
|
pytz>=2016.10
|
||||||
requests>=2.28.0,<2.32.0
|
requests>=2.28.0,<2.32.0
|
||||||
SQLAlchemy>=1.3.0,<2.1.0
|
SQLAlchemy>=1.3.0,<2.1.0
|
||||||
tornado>=6.3,<6.4
|
tornado>=6.3,<6.5
|
||||||
Wand>=0.4.4,<0.7.0
|
Wand>=0.4.4,<0.7.0
|
||||||
unidecode>=0.04.19,<1.4.0
|
unidecode>=0.04.19,<1.4.0
|
||||||
lxml>=3.8.0,<5.0.0
|
lxml>=3.8.0,<5.2.0
|
||||||
flask-wtf>=0.14.2,<1.3.0
|
flask-wtf>=0.14.2,<1.3.0
|
||||||
chardet>=3.0.0,<4.1.0
|
chardet>=3.0.0,<4.1.0
|
||||||
advocate>=1.0.0,<1.1.0
|
advocate>=1.0.0,<1.1.0
|
||||||
|
|
26
setup.cfg
26
setup.cfg
|
@ -42,18 +42,18 @@ install_requires =
|
||||||
APScheduler>=3.6.3,<3.11.0
|
APScheduler>=3.6.3,<3.11.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<4.1.0
|
Flask-Babel>=0.11.1,<4.1.0
|
||||||
Flask-Login>=0.3.2,<0.6.3
|
Flask-Login>=0.3.2,<0.6.4
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
Flask>=1.0.2,<2.4.0
|
Flask>=1.0.2,<3.1.0
|
||||||
iso-639>=0.4.5,<0.5.0
|
iso-639>=0.4.5,<0.5.0
|
||||||
PyPDF>=3.0.0,<3.16.0
|
PyPDF>=3.15.6,<4.1.0
|
||||||
pytz>=2016.10
|
pytz>=2016.10
|
||||||
requests>=2.28.0,<2.32.0
|
requests>=2.28.0,<2.32.0
|
||||||
SQLAlchemy>=1.3.0,<2.1.0
|
SQLAlchemy>=1.3.0,<2.1.0
|
||||||
tornado>=6.3,<6.4
|
tornado>=6.3,<6.5
|
||||||
Wand>=0.4.4,<0.7.0
|
Wand>=0.4.4,<0.7.0
|
||||||
unidecode>=0.04.19,<1.4.0
|
unidecode>=0.04.19,<1.4.0
|
||||||
lxml>=3.8.0,<5.0.0
|
lxml>=3.8.0,<5.2.0
|
||||||
flask-wtf>=0.14.2,<1.3.0
|
flask-wtf>=0.14.2,<1.3.0
|
||||||
chardet>=3.0.0,<4.1.0
|
chardet>=3.0.0,<4.1.0
|
||||||
advocate>=1.0.0,<1.1.0
|
advocate>=1.0.0,<1.1.0
|
||||||
|
@ -66,23 +66,23 @@ include = cps/services*
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
gdrive =
|
gdrive =
|
||||||
google-api-python-client>=1.7.11,<2.108.0
|
google-api-python-client>=1.7.11,<2.120.0
|
||||||
gevent>20.6.0,<24.0.0
|
gevent>20.6.0,<24.3.0
|
||||||
greenlet>=0.4.17,<3.1.0
|
greenlet>=0.4.17,<3.1.0
|
||||||
httplib2>=0.9.2,<0.23.0
|
httplib2>=0.9.2,<0.23.0
|
||||||
oauth2client>=4.0.0,<4.1.4
|
oauth2client>=4.0.0,<4.1.4
|
||||||
uritemplate>=3.0.0,<4.2.0
|
uritemplate>=3.0.0,<4.2.0
|
||||||
pyasn1-modules>=0.0.8,<0.4.0
|
pyasn1-modules>=0.0.8,<0.4.0
|
||||||
pyasn1>=0.1.9,<0.6.0
|
pyasn1>=0.1.9,<0.6.0
|
||||||
PyDrive2>=1.3.1,<1.18.0
|
PyDrive2>=1.3.1,<1.20.0
|
||||||
PyYAML>=3.12,<6.1
|
PyYAML>=3.12,<6.1
|
||||||
rsa>=3.4.2,<4.10.0
|
rsa>=3.4.2,<4.10.0
|
||||||
gmail =
|
gmail =
|
||||||
google-auth-oauthlib>=0.4.3,<1.1.0
|
google-auth-oauthlib>=0.4.3,<1.3.0
|
||||||
google-api-python-client>=1.7.11,<2.108.0
|
google-api-python-client>=1.7.11,<2.120.0
|
||||||
goodreads =
|
goodreads =
|
||||||
goodreads>=0.3.2,<0.4.0
|
goodreads>=0.3.2,<0.4.0
|
||||||
python-Levenshtein>=0.12.0,<0.22.0
|
python-Levenshtein>=0.12.0,<0.26.0
|
||||||
ldap =
|
ldap =
|
||||||
python-ldap>=3.0.0,<3.5.0
|
python-ldap>=3.0.0,<3.5.0
|
||||||
Flask-SimpleLDAP>=1.4.0,<1.5.0
|
Flask-SimpleLDAP>=1.4.0,<1.5.0
|
||||||
|
@ -90,10 +90,10 @@ oauth =
|
||||||
Flask-Dance>=2.0.0,<7.1.0
|
Flask-Dance>=2.0.0,<7.1.0
|
||||||
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
||||||
metadata =
|
metadata =
|
||||||
rarfile>=3.2
|
rarfile>=3.2,<4.2
|
||||||
scholarly>=1.2.0,<1.8
|
scholarly>=1.2.0,<1.8
|
||||||
markdown2>=2.0.0,<2.5.0
|
markdown2>=2.0.0,<2.5.0
|
||||||
html2text>=2020.1.16,<2022.1.1
|
html2text>=2020.1.16,<2024.2.26
|
||||||
python-dateutil>=2.1,<2.9.0
|
python-dateutil>=2.1,<2.9.0
|
||||||
beautifulsoup4>=4.0.1,<4.13.0
|
beautifulsoup4>=4.0.1,<4.13.0
|
||||||
faust-cchardet>=2.1.18,<2.1.20
|
faust-cchardet>=2.1.18,<2.1.20
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user