Merge remote-tracking branch 'upstream/master' into tasks

# Conflicts:
#	cps/helper.py
pull/1580/head
blitzmann 4 years ago
commit 6a8ae9c0c4

@ -132,6 +132,7 @@ def admin():
allUser = ub.session.query(ub.User).all() allUser = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings() email_settings = config.get_mail_settings()
return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit, return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit,
feature_support=feature_support,
title=_(u"Admin page"), page="admin") title=_(u"Admin page"), page="admin")
@ -637,6 +638,7 @@ def _configuration_update_helper():
_config_checkbox_int(to_save, "config_public_reg") _config_checkbox_int(to_save, "config_public_reg")
_config_checkbox_int(to_save, "config_register_email") _config_checkbox_int(to_save, "config_register_email")
reboot_required |= _config_checkbox_int(to_save, "config_kobo_sync") reboot_required |= _config_checkbox_int(to_save, "config_kobo_sync")
_config_int(to_save, "config_external_port")
_config_checkbox_int(to_save, "config_kobo_proxy") _config_checkbox_int(to_save, "config_kobo_proxy")
_config_string(to_save, "config_upload_formats") _config_string(to_save, "config_upload_formats")

@ -57,6 +57,7 @@ class _Settings(_Base):
config_calibre_dir = Column(String) config_calibre_dir = Column(String)
config_port = Column(Integer, default=constants.DEFAULT_PORT) config_port = Column(Integer, default=constants.DEFAULT_PORT)
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
config_certfile = Column(String) config_certfile = Column(String)
config_keyfile = Column(String) config_keyfile = Column(String)

@ -377,7 +377,8 @@ def edit_book_publisher(to_save, book):
if to_save["publisher"]: if to_save["publisher"]:
publisher = to_save["publisher"].rstrip().strip() publisher = to_save["publisher"].rstrip().strip()
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session, 'publisher') changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
'publisher')
elif len(book.publishers): elif len(book.publishers):
changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher') changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher')
return changed return changed

@ -22,6 +22,7 @@ import zipfile
from lxml import etree from lxml import etree
from . import isoLanguages from . import isoLanguages
from .helper import split_authors
from .constants import BookMeta from .constants import BookMeta
@ -64,7 +65,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
if len(tmp) > 0: if len(tmp) > 0:
if s == 'creator': if s == 'creator':
epub_metadata[s] = ' & '.join(p.xpath('dc:%s/text()' % s, namespaces=ns)) epub_metadata[s] = ' & '.join(split_authors(p.xpath('dc:%s/text()' % s, namespaces=ns)))
elif s == 'subject': elif s == 'subject':
epub_metadata[s] = ', '.join(p.xpath('dc:%s/text()' % s, namespaces=ns)) epub_metadata[s] = ', '.join(p.xpath('dc:%s/text()' % s, namespaces=ns))
else: else:

@ -21,7 +21,6 @@ from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import io import io
import json
import mimetypes import mimetypes
import re import re
import shutil import shutil
@ -36,7 +35,7 @@ from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort from flask import send_from_directory, make_response, redirect, abort
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, or_, text, func from sqlalchemy.sql.expression import true, false, and_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from . import calibre_db from . import calibre_db
@ -60,10 +59,9 @@ try:
except ImportError: except ImportError:
use_PIL = False use_PIL = False
from . import logger, config, get_locale, db, ub, isoLanguages from . import logger, config, get_locale, db, ub
from . import gdriveutils as gd from . import gdriveutils as gd
from .constants import STATIC_DIR as _STATIC_DIR from .constants import STATIC_DIR as _STATIC_DIR
from .pagination import Pagination
from .subproc_wrapper import process_wait from .subproc_wrapper import process_wait
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
from .tasks.email import TaskEmail from .tasks.email import TaskEmail
@ -240,22 +238,22 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:-1]+u'_' value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0') value = value.replace("/", "_").replace(":", "_").strip('\0')
if use_unidecode: if use_unidecode:
value = (unidecode.unidecode(value)).strip() value = (unidecode.unidecode(value))
else: else:
value = value.replace(u'§', u'SS') value = value.replace(u'§', u'SS')
value = value.replace(u'ß', u'ss') value = value.replace(u'ß', u'ss')
value = unicodedata.normalize('NFKD', value) value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile(r'[\W\s-]', re.UNICODE) re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
if isinstance(value, str): # Python3 str, Python2 unicode if isinstance(value, str): # Python3 str, Python2 unicode
value = re_slugify.sub('', value).strip() value = re_slugify.sub('', value)
else: else:
value = unicode(re_slugify.sub('', value).strip()) value = unicode(re_slugify.sub('', value))
if replace_whitespace: if replace_whitespace:
# *+:\"/<>? are replaced by _ # *+:\"/<>? are replaced by _
value = re.sub(r'[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U) value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
# pipe has to be replaced with comma # pipe has to be replaced with comma
value = re.sub(r'[\|]+', u',', value, flags=re.U) value = re.sub(r'[|]+', u',', value, flags=re.U)
value = value[:128] value = value[:128].strip()
if not value: if not value:
raise ValueError("Filename cannot be empty") raise ValueError("Filename cannot be empty")
if sys.version_info.major == 3: if sys.version_info.major == 3:
@ -264,6 +262,22 @@ def get_valid_filename(value, replace_whitespace=True):
return value.decode('utf-8') return value.decode('utf-8')
def split_authors(values):
authors_list = []
for value in values:
authors = re.split('[&;]', value)
for author in authors:
commas = author.count(',')
if commas == 1:
author_split = author.split(',')
authors_list.append(author_split[1].strip() + ' ' + author_split[0].strip())
elif commas > 1:
authors_list.extend([x.strip() for x in author.split(',')])
else:
authors_list.append(author.strip())
return authors_list
def get_sorted_author(value): def get_sorted_author(value):
try: try:
if ',' not in value: if ',' not in value:
@ -271,7 +285,10 @@ def get_sorted_author(value):
combined = "(" + ")|(".join(regexes) + ")" combined = "(" + ")|(".join(regexes) + ")"
value = value.split(" ") value = value.split(" ")
if re.match(combined, value[-1].upper()): if re.match(combined, value[-1].upper()):
if len(value) > 1:
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1] value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
else:
value2 = value[0]
elif len(value) == 1: elif len(value) == 1:
value2 = value[0] value2 = value[0]
else: else:
@ -280,6 +297,9 @@ def get_sorted_author(value):
value2 = value value2 = value
except Exception as ex: except Exception as ex:
log.error("Sorting author %s failed: %s", value, ex) log.error("Sorting author %s failed: %s", value, ex)
if isinstance(list, value2):
value2 = value[0]
else:
value2 = value value2 = value
return value2 return value2
@ -367,6 +387,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
src=path, dest=new_author_path, error=str(ex)) src=path, dest=new_author_path, error=str(ex))
# Rename all files from old names to new names # Rename all files from old names to new names
if authordir != new_authordir or titledir != new_titledir: if authordir != new_authordir or titledir != new_titledir:
new_name = ""
try: try:
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir) new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path)) path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
@ -475,14 +496,14 @@ def generate_random_password():
return "".join(s[c % len(s)] for c in os.urandom(passlen)) return "".join(s[c % len(s)] for c in os.urandom(passlen))
def uniq(input): def uniq(inpt):
output = [] output = []
for x in input: for x in inpt:
if x not in output: if x not in output:
output.append(x) output.append(x)
return output return output
################################## External interface # ################################# External interface #################################
def update_dir_stucture(book_id, calibrepath, first_author=None): def update_dir_stucture(book_id, calibrepath, first_author=None):
@ -559,7 +580,6 @@ def save_cover_from_url(url, book_path):
return False, _("Cover Format Error") return False, _("Cover Format Error")
def save_cover_from_filestorage(filepath, saved_filename, img): def save_cover_from_filestorage(filepath, saved_filename, img):
if hasattr(img, '_content'): if hasattr(img, '_content'):
f = open(os.path.join(filepath, saved_filename), "wb") f = open(os.path.join(filepath, saved_filename), "wb")
@ -618,7 +638,6 @@ def save_cover(img, book_path):
return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img)
def do_download_file(book, book_format, client, data, headers): def do_download_file(book, book_format, client, data, headers):
if config.config_use_google_drive: if config.config_use_google_drive:
startTime = time.time() startTime = time.time()
@ -771,6 +790,7 @@ def get_cc_columns(filter_config_custom_read=False):
return cc return cc
def get_download_link(book_id, book_format, client): def get_download_link(book_id, book_format, client):
book_format = book_format.split(".")[0] book_format = book_format.split(".")[0]
book = calibre_db.get_filtered_book(book_id) book = calibre_db.get_filtered_book(book_id)

@ -129,7 +129,7 @@ def HandleSyncRequest():
sync_token = SyncToken.SyncToken.from_headers(request.headers) sync_token = SyncToken.SyncToken.from_headers(request.headers)
log.info("Kobo library sync request received.") log.info("Kobo library sync request received.")
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header # TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
# instead so that the device triggers another sync. # instead so that the device triggers another sync.
@ -252,7 +252,7 @@ def generate_sync_response(sync_token, sync_results):
@download_required @download_required
def HandleMetadataRequest(book_uuid): def HandleMetadataRequest(book_uuid):
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
log.info("Kobo library metadata request received for book %s" % book_uuid) log.info("Kobo library metadata request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data: if not book or not book.data:
@ -269,10 +269,11 @@ def get_download_url_for_book(book, book_format):
host = "".join(request.host.split(':')[:-1]) host = "".join(request.host.split(':')[:-1])
else: else:
host = request.host host = request.host
return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format( return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format(
url_scheme=request.scheme, url_scheme=request.scheme,
url_base=host, url_base=host,
url_port=config.config_port, url_port=config.config_external_port,
book_id=book.id, book_id=book.id,
book_format=book_format.lower() book_format=book_format.lower()
) )
@ -924,7 +925,7 @@ def HandleInitRequest():
kobo_resources = NATIVE_KOBO_RESOURCES() kobo_resources = NATIVE_KOBO_RESOURCES()
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
if ':' in request.host and not request.host.endswith(']'): if ':' in request.host and not request.host.endswith(']'):
host = "".join(request.host.split(':')[:-1]) host = "".join(request.host.split(':')[:-1])
else: else:
@ -932,8 +933,9 @@ def HandleInitRequest():
calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format( calibre_web_url = "{url_scheme}://{url_base}:{url_port}".format(
url_scheme=request.scheme, url_scheme=request.scheme,
url_base=host, url_base=host,
url_port=config.config_port url_port=config.config_external_port
) )
log.debug('Kobo: Received unproxied request, changed request url to %s', calibre_web_url)
kobo_resources["image_host"] = calibre_web_url kobo_resources["image_host"] = calibre_web_url
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url + kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
url_for("kobo.HandleCoverImageRequest", url_for("kobo.HandleCoverImageRequest",
@ -942,16 +944,14 @@ def HandleInitRequest():
width="{width}", width="{width}",
height="{height}", height="{height}",
Quality='{Quality}', Quality='{Quality}',
isGreyscale='isGreyscale' isGreyscale='isGreyscale'))
))
kobo_resources["image_url_template"] = unquote(calibre_web_url + kobo_resources["image_url_template"] = unquote(calibre_web_url +
url_for("kobo.HandleCoverImageRequest", url_for("kobo.HandleCoverImageRequest",
auth_token=kobo_auth.get_auth_token(), auth_token=kobo_auth.get_auth_token(),
book_uuid="{ImageId}", book_uuid="{ImageId}",
width="{width}", width="{width}",
height="{height}", height="{height}",
isGreyscale='false' isGreyscale='false'))
))
else: else:
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/") kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
@ -970,7 +970,6 @@ def HandleInitRequest():
isGreyscale='false', isGreyscale='false',
_external=True)) _external=True))
response = make_response(jsonify({"Resources": kobo_resources})) response = make_response(jsonify({"Resources": kobo_resources}))
response.headers["x-kobo-apitoken"] = "e30=" response.headers["x-kobo-apitoken"] = "e30="

@ -88,6 +88,12 @@
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div> <div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div> <div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
</div> </div>
{% if feature_support['kobo'] and config.config_port != config.config_external_port %}
<div class="row">
<div class="col-xs-6 col-sm-6">{{_('External Port')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_external_port}}</div>
</div>
{% endif %}
</div> </div>
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="row"> <div class="row">

@ -194,10 +194,14 @@
<label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label> <label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label>
</div> </div>
<div data-related="kobo-settings"> <div data-related="kobo-settings">
<div class="form-group" style="text-indent:10px;"> <div class="form-group" style="margin-left:10px;">
<input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}> <input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}>
<label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label> <label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label>
</div> </div>
<div class="form-group" style="margin-left:10px;">
<label for="config_external_port">{{_('Server External Port (for port forwarded API calls)')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_external_port" id="config_external_port" value="{% if config.config_external_port != None %}{{ config.config_external_port }}{% endif %}" autocomplete="off" required>
</div>
</div> </div>
{% endif %} {% endif %}
{% if feature_support['goodreads'] %} {% if feature_support['goodreads'] %}

@ -17,7 +17,7 @@
{{entry['series_index']}} - {{entry['series'][0].name}} {{entry['series_index']}} - {{entry['series'][0].name}}
{% endif %} {% endif %}
<br> <br>
{% for author in entry['authors'] %} {% for author in entry['author'] %}
{{author.name.replace('|',',')}} {{author.name.replace('|',',')}}
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;

@ -24,6 +24,7 @@ from flask_babel import gettext as _
from . import logger, comic from . import logger, comic
from .constants import BookMeta from .constants import BookMeta
from .helper import split_authors
log = logger.create() log = logger.create()
@ -131,7 +132,7 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
file_path=tmp_file_path, file_path=tmp_file_path,
extension=original_file_extension, extension=original_file_extension,
title=title, title=title,
author=author, author=' & '.join(split_authors([author])),
cover=pdf_preview(tmp_file_path, original_file_name), cover=pdf_preview(tmp_file_path, original_file_name),
description=subject, description=subject,
tags="", tags="",

@ -1361,7 +1361,7 @@ def register():
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
else: else:
flash(_(u"Your e-mail is not allowed to register"), category="error") flash(_(u"Your e-mail is not allowed to register"), category="error")
log.info('Registering failed for user "%s" e-mail adress: %s', to_save['nickname'], to_save["email"]) log.warning('Registering failed for user "%s" e-mail address: %s', to_save['nickname'], to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success") flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
@ -1409,7 +1409,7 @@ def login():
flash(_(u"Could not login: %(message)s", message=error), category="error") flash(_(u"Could not login: %(message)s", message=error), category="error")
else: else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
log.info('LDAP Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ipAdress)
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
else: else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
@ -1418,13 +1418,13 @@ def login():
ret, __ = reset_password(user.id) ret, __ = reset_password(user.id)
if ret == 1: if ret == 1:
flash(_(u"New Password was send to your email address"), category="info") flash(_(u"New Password was send to your email address"), category="info")
log.info('Password reset for user "%s" IP-adress: %s', form['username'], ipAdress) log.info('Password reset for user "%s" IP-address: %s', form['username'], ipAdress)
else: else:
log.info(u"An unknown error occurred. Please try again later") log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_(u"An unknown error occurred. Please try again later."), category="error")
else: else:
flash(_(u"Please enter valid username to reset password"), category="error") flash(_(u"Please enter valid username to reset password"), category="error")
log.info('Username missing for password reset IP-adress: %s', ipAdress) log.warning('Username missing for password reset IP-address: %s', ipAdress)
else: else:
if user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": if user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest":
login_user(user, remember=bool(form.get('remember_me'))) login_user(user, remember=bool(form.get('remember_me')))
@ -1433,7 +1433,7 @@ def login():
config.config_is_initial = False config.config_is_initial = False
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
else: else:
log.info('Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) log.warning('Login failed for user "%s" IP-address: %s', form['username'], ipAdress)
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
next_url = request.args.get('next', default=url_for("web.index"), type=str) next_url = request.args.get('next', default=url_for("web.index"), type=str)

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