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()
|
||||
|
||||
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:
|
||||
limiter = None
|
||||
|
||||
|
@ -196,8 +196,18 @@ def create_app():
|
|||
config.config_use_goodreads)
|
||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
||||
# Configure rate limiter
|
||||
# https://limits.readthedocs.io/en/stable/storage.html
|
||||
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
|
||||
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, \
|
||||
kobo_sync_status, schedule
|
||||
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 .render_template import render_title_template, get_sidebar_config
|
||||
from .services.worker import WorkerThread
|
||||
|
@ -1716,7 +1717,7 @@ def _db_configuration_update_helper():
|
|||
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
||||
|
||||
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']:
|
||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
|
||||
else:
|
||||
|
@ -1840,6 +1841,8 @@ def _configuration_update_helper():
|
|||
return _configuration_result(_('Password length has to be between 1 and 40'))
|
||||
reboot_required |= _config_int(to_save, "config_session")
|
||||
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
|
||||
_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',
|
||||
version=version_info())
|
||||
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',
|
||||
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')
|
||||
|
@ -98,6 +99,8 @@ class CliParameter(object):
|
|||
if args.k == "":
|
||||
self.keyfilepath = ""
|
||||
|
||||
# overwrite limiter backend
|
||||
self.memory_backend = args.m or None
|
||||
# dry run updater
|
||||
self.dry_run = args.d or None
|
||||
# enable reconnect endpoint for docker database reconnect
|
||||
|
|
|
@ -168,6 +168,8 @@ class _Settings(_Base):
|
|||
config_password_special = Column(Boolean, default=True)
|
||||
config_session = Column(Integer, default=1)
|
||||
config_ratelimiter = Column(Boolean, default=True)
|
||||
config_limiter_uri = Column(String, default="")
|
||||
config_limiter_options = Column(String, default="")
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
|
|
@ -60,6 +60,7 @@ from .tasks.upload import TaskUpload
|
|||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .kobo_sync_status import change_archived_books
|
||||
from .redirect import get_redirect_location
|
||||
|
||||
|
||||
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"])
|
||||
@login_required
|
||||
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'])
|
||||
|
@ -823,7 +824,7 @@ def delete_whole_book(book_id, book):
|
|||
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 json_response:
|
||||
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))
|
||||
else:
|
||||
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",
|
||||
"format": book_format,
|
||||
"message": _('Book Successfully Deleted')}])
|
||||
else:
|
||||
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 = {}
|
||||
if current_user.role_delete_books():
|
||||
book = calibre_db.get_book(book_id)
|
||||
|
@ -891,7 +892,7 @@ def delete_book_from_table(book_id, book_format, json_response):
|
|||
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, 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")
|
||||
if json_response:
|
||||
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 unidecode
|
||||
from uuid import uuid4
|
||||
from lxml import etree
|
||||
|
||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||
from flask_babel import gettext as _
|
||||
|
@ -56,13 +55,14 @@ from .tasks.convert import TaskConvert
|
|||
from . import logger, config, db, ub, fs
|
||||
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 .subproc_wrapper import process_wait, process_open
|
||||
from .subproc_wrapper import process_wait
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.mail import TaskEmail
|
||||
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
|
||||
from .tasks.metadata_backup import TaskBackupMetadata
|
||||
from .file_helper import get_temp_dir
|
||||
from .epub_helper import get_content_opf, create_new_metadata_backup, updateEpub, replace_metadata
|
||||
from .embed_helper import do_calibre_export
|
||||
|
||||
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)
|
||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||
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 _("The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
|
@ -692,15 +692,15 @@ def valid_password(check_password):
|
|||
if config.config_password_policy:
|
||||
verify = ""
|
||||
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:
|
||||
verify += "(?=.*?\d)"
|
||||
verify += r"(?=.*?\d)"
|
||||
if config.config_password_lower:
|
||||
verify += "(?=.*?[a-z])"
|
||||
verify += r"(?=.*?[a-z])"
|
||||
if config.config_password_upper:
|
||||
verify += "(?=.*?[A-Z])"
|
||||
verify += r"(?=.*?[A-Z])"
|
||||
if config.config_password_special:
|
||||
verify += "(?=.*?[^A-Za-z\s0-9])"
|
||||
verify += r"(?=.*?[^A-Za-z\s0-9])"
|
||||
match = re.match(verify, check_password)
|
||||
if not match:
|
||||
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
|
||||
|
||||
|
||||
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_executable = [os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths]
|
||||
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]
|
||||
if all(values):
|
||||
version = values[0].group(1)
|
||||
|
@ -1149,17 +1122,6 @@ def get_download_link(book_id, book_format, client):
|
|||
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):
|
||||
if config.schedule_generate_book_covers:
|
||||
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True)
|
||||
|
|
|
@ -156,6 +156,9 @@ def requires_kobo_auth(f):
|
|||
limiter.check()
|
||||
except RateLimitExceeded:
|
||||
return abort(429)
|
||||
except (ConnectionError, Exception) as e:
|
||||
log.error("Connection error to limiter backend: %s", e)
|
||||
return abort(429)
|
||||
user = (
|
||||
ub.session.query(ub.User)
|
||||
.join(ub.RemoteAuthToken)
|
||||
|
|
|
@ -44,9 +44,9 @@ def remove_prefix(text, prefix):
|
|||
return ""
|
||||
|
||||
|
||||
def redirect_back(endpoint, **values):
|
||||
target = request.form.get('next', None) or url_for(endpoint, **values)
|
||||
def get_redirect_location(next, endpoint, **values):
|
||||
target = next or url_for(endpoint, **values)
|
||||
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',"")))):
|
||||
target = url_for(endpoint, **values)
|
||||
return redirect(target)
|
||||
return target
|
||||
|
|
|
@ -266,3 +266,6 @@ class CalibreTask:
|
|||
def _handleSuccess(self):
|
||||
self.stat = STAT_FINISH_SUCCESS
|
||||
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
|
||||
}
|
||||
|
||||
function postButton(event, action){
|
||||
function postButton(event, action, location=""){
|
||||
event.preventDefault();
|
||||
var newForm = jQuery('<form>', {
|
||||
"action": action,
|
||||
|
@ -30,7 +30,14 @@ function postButton(event, action){
|
|||
'name': 'csrf_token',
|
||||
'value': $("input[name=\'csrf_token\']").val(),
|
||||
'type': 'hidden'
|
||||
})).appendTo('body');
|
||||
})).appendTo('body')
|
||||
if(location !== "") {
|
||||
newForm.append(jQuery('<input>', {
|
||||
'name': 'location',
|
||||
'value': location,
|
||||
'type': 'hidden'
|
||||
})).appendTo('body');
|
||||
}
|
||||
newForm.submit();
|
||||
}
|
||||
|
||||
|
@ -212,17 +219,20 @@ $("#delete_confirm").click(function(event) {
|
|||
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
|
||||
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
||||
'</div>');
|
||||
|
||||
}
|
||||
});
|
||||
$("#books-table").bootstrapTable("refresh");
|
||||
}
|
||||
});
|
||||
} 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
|
||||
|
@ -541,6 +551,7 @@ $(function() {
|
|||
$.get(e.relatedTarget.href).done(function(content) {
|
||||
$modalBody.html(content);
|
||||
preFilters.remove(useCache);
|
||||
$("#back").remove();
|
||||
});
|
||||
})
|
||||
.on("hidden.bs.modal", function() {
|
||||
|
|
|
@ -110,6 +110,7 @@ class TaskConvert(CalibreTask):
|
|||
self.ereader_mail,
|
||||
EmailText,
|
||||
self.settings['body'],
|
||||
id=self.book_id,
|
||||
internal=True)
|
||||
)
|
||||
except Exception as ex:
|
||||
|
@ -315,9 +316,9 @@ class TaskConvert(CalibreTask):
|
|||
|
||||
def __str__(self):
|
||||
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:
|
||||
return "Convert {}".format(self.book_id)
|
||||
return "Convert Book {}".format(self.book_id)
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
|
|
|
@ -28,12 +28,11 @@ from email.message import EmailMessage
|
|||
from email.utils import formatdate, parseaddr
|
||||
from email.generator import Generator
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from email.utils import formatdate
|
||||
|
||||
from cps.services.worker import CalibreTask
|
||||
from cps.services import gmail
|
||||
from cps.embed_helper import do_calibre_export
|
||||
from cps import logger, config
|
||||
|
||||
from cps import gdriveutils
|
||||
import uuid
|
||||
|
||||
|
@ -110,7 +109,7 @@ class EmailSSL(EmailBase, smtplib.SMTP_SSL):
|
|||
|
||||
|
||||
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)
|
||||
self.subject = subject
|
||||
self.attachment = attachment
|
||||
|
@ -119,6 +118,7 @@ class TaskEmail(CalibreTask):
|
|||
self.recipient = recipient
|
||||
self.text = text
|
||||
self.asyncSMTP = None
|
||||
self.book_id = id
|
||||
self.results = dict()
|
||||
|
||||
# from calibre code:
|
||||
|
@ -141,7 +141,7 @@ class TaskEmail(CalibreTask):
|
|||
message['To'] = self.recipient
|
||||
message['Subject'] = self.subject
|
||||
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")
|
||||
if self.attachment:
|
||||
data = self._get_attachment(self.filepath, self.attachment)
|
||||
|
@ -161,6 +161,8 @@ class TaskEmail(CalibreTask):
|
|||
try:
|
||||
# create MIME message
|
||||
msg = self.prepare_message()
|
||||
if not msg:
|
||||
return
|
||||
if self.settings['mail_server_type'] == 0:
|
||||
self.send_standard_email(msg)
|
||||
else:
|
||||
|
@ -236,10 +238,10 @@ class TaskEmail(CalibreTask):
|
|||
self.asyncSMTP = None
|
||||
self._progress = x
|
||||
|
||||
@classmethod
|
||||
def _get_attachment(cls, book_path, filename):
|
||||
def _get_attachment(self, book_path, filename):
|
||||
"""Get file as MIMEBase message"""
|
||||
calibre_path = config.get_book_path()
|
||||
extension = os.path.splitext(filename)[1][1:]
|
||||
if config.config_use_google_drive:
|
||||
df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
|
||||
if df:
|
||||
|
@ -249,15 +251,22 @@ class TaskEmail(CalibreTask):
|
|||
df.GetContentFile(datafile)
|
||||
else:
|
||||
return None
|
||||
file_ = open(datafile, 'rb')
|
||||
data = file_.read()
|
||||
file_.close()
|
||||
if config.config_binariesdir and config.config_embed_metadata:
|
||||
data_path, data_file = do_calibre_export(self.book_id, extension)
|
||||
datafile = os.path.join(data_path, data_file + "." + extension)
|
||||
with open(datafile, 'rb') as file_:
|
||||
data = file_.read()
|
||||
os.remove(datafile)
|
||||
else:
|
||||
datafile = os.path.join(calibre_path, book_path, filename)
|
||||
try:
|
||||
file_ = open(os.path.join(calibre_path, book_path, filename), 'rb')
|
||||
data = file_.read()
|
||||
file_.close()
|
||||
if config.config_binariesdir and config.config_embed_metadata:
|
||||
data_path, data_file = do_calibre_export(self.book_id, extension)
|
||||
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:
|
||||
log.error_or_exception(e, stacklevel=3)
|
||||
log.error('The requested file could not be read. Maybe wrong permissions?')
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="row display-flex">
|
||||
{% 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">
|
||||
<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}}">
|
||||
|
@ -99,7 +99,7 @@
|
|||
<h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
|
||||
<div class="row">
|
||||
{% 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">
|
||||
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
|
||||
<img title="{{entry.title}}" src="{{ entry.image_url }}" />
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<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 %} >
|
||||
<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 data-related="split_settings">
|
||||
<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 class="form-group">
|
||||
<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 class="form-group">
|
||||
<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 %}>
|
||||
<label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label>
|
||||
</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">
|
||||
<label for="config_session">{{_('Session protection')}}</label>
|
||||
<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 %}
|
||||
{% 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">
|
||||
<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="glyphicon glyphicon-edit"></span> {{ _('Edit Metadata') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-default" data-back="{{ url_for('web.index') }}" id="back">{{_('Cancel')}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -367,4 +367,3 @@
|
|||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
|
||||
<div class="row display-flex">
|
||||
{% 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">
|
||||
<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 }}">
|
||||
|
@ -89,7 +89,7 @@
|
|||
<div class="row display-flex">
|
||||
{% if entries[0] %}
|
||||
{% 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">
|
||||
<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 }}">
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<div class="row display-flex">
|
||||
{% 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">
|
||||
{% 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 %}>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
{% endif %}
|
||||
<div class="row display-flex">
|
||||
{% 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">
|
||||
<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}}" >
|
||||
|
|
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, \
|
||||
edit_book_read_status, valid_password
|
||||
from .pagination import Pagination
|
||||
from .redirect import redirect_back
|
||||
from .redirect import get_redirect_location
|
||||
from .babel import get_available_locale
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .kobo_sync_status import remove_synced_book
|
||||
|
@ -1276,6 +1276,10 @@ def register_post():
|
|||
except RateLimitExceeded:
|
||||
flash(_(u"Please wait one minute to register next user"), category="error")
|
||||
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:
|
||||
return redirect(url_for('web.index'))
|
||||
if not config.get_mail_server_configured():
|
||||
|
@ -1338,7 +1342,7 @@ def handle_login_user(user, remember, message, category):
|
|||
ub.store_user_session()
|
||||
flash(message, category=category)
|
||||
[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=""):
|
||||
|
@ -1374,7 +1378,11 @@ def login_post():
|
|||
try:
|
||||
limiter.check()
|
||||
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", ""))
|
||||
if current_user is not None and current_user.is_authenticated:
|
||||
return redirect(url_for('web.index'))
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
# GDrive Integration
|
||||
google-api-python-client>=1.7.11,<2.108.0
|
||||
gevent>20.6.0,<24.0.0
|
||||
google-api-python-client>=1.7.11,<2.120.0
|
||||
gevent>20.6.0,<24.3.0
|
||||
greenlet>=0.4.17,<3.1.0
|
||||
httplib2>=0.9.2,<0.23.0
|
||||
oauth2client>=4.0.0,<4.1.4
|
||||
uritemplate>=3.0.0,<4.2.0
|
||||
pyasn1-modules>=0.0.8,<0.4.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
|
||||
rsa>=3.4.2,<4.10.0
|
||||
|
||||
# Gmail
|
||||
google-auth-oauthlib>=0.4.3,<1.1.0
|
||||
google-api-python-client>=1.7.11,<2.108.0
|
||||
google-auth-oauthlib>=0.4.3,<1.3.0
|
||||
google-api-python-client>=1.7.11,<2.120.0
|
||||
|
||||
# goodreads
|
||||
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
|
||||
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
|
||||
|
||||
# metadata extraction
|
||||
rarfile>=3.2
|
||||
rarfile>=3.2,<4.2
|
||||
scholarly>=1.2.0,<1.8
|
||||
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
|
||||
beautifulsoup4>=4.0.1,<4.13.0
|
||||
faust-cchardet>=2.1.18,<2.1.20
|
||||
|
|
|
@ -2,18 +2,18 @@ Werkzeug<3.0.0
|
|||
APScheduler>=3.6.3,<3.11.0
|
||||
Babel>=1.3,<3.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>=1.0.2,<2.4.0
|
||||
Flask>=1.0.2,<3.1.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
|
||||
requests>=2.28.0,<2.32.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
|
||||
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
|
||||
chardet>=3.0.0,<4.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
|
||||
Babel>=1.3,<3.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>=1.0.2,<2.4.0
|
||||
Flask>=1.0.2,<3.1.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
|
||||
requests>=2.28.0,<2.32.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
|
||||
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
|
||||
chardet>=3.0.0,<4.1.0
|
||||
advocate>=1.0.0,<1.1.0
|
||||
|
@ -66,23 +66,23 @@ include = cps/services*
|
|||
|
||||
[options.extras_require]
|
||||
gdrive =
|
||||
google-api-python-client>=1.7.11,<2.108.0
|
||||
gevent>20.6.0,<24.0.0
|
||||
google-api-python-client>=1.7.11,<2.120.0
|
||||
gevent>20.6.0,<24.3.0
|
||||
greenlet>=0.4.17,<3.1.0
|
||||
httplib2>=0.9.2,<0.23.0
|
||||
oauth2client>=4.0.0,<4.1.4
|
||||
uritemplate>=3.0.0,<4.2.0
|
||||
pyasn1-modules>=0.0.8,<0.4.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
|
||||
rsa>=3.4.2,<4.10.0
|
||||
gmail =
|
||||
google-auth-oauthlib>=0.4.3,<1.1.0
|
||||
google-api-python-client>=1.7.11,<2.108.0
|
||||
google-auth-oauthlib>=0.4.3,<1.3.0
|
||||
google-api-python-client>=1.7.11,<2.120.0
|
||||
goodreads =
|
||||
goodreads>=0.3.2,<0.4.0
|
||||
python-Levenshtein>=0.12.0,<0.22.0
|
||||
python-Levenshtein>=0.12.0,<0.26.0
|
||||
ldap =
|
||||
python-ldap>=3.0.0,<3.5.0
|
||||
Flask-SimpleLDAP>=1.4.0,<1.5.0
|
||||
|
@ -90,10 +90,10 @@ oauth =
|
|||
Flask-Dance>=2.0.0,<7.1.0
|
||||
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
||||
metadata =
|
||||
rarfile>=3.2
|
||||
rarfile>=3.2,<4.2
|
||||
scholarly>=1.2.0,<1.8
|
||||
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
|
||||
beautifulsoup4>=4.0.1,<4.13.0
|
||||
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