diff --git a/cps/__init__.py b/cps/__init__.py
index 269599ad..0a452b17 100644
--- a/cps/__init__.py
+++ b/cps/__init__.py
@@ -23,32 +23,22 @@ import sys
import os
import mimetypes
-from babel import Locale as LC
-from babel import negotiate_locale
-from babel.core import UnknownLocaleError
-from flask import request, g
from flask import Flask
from .MyLoginManager import MyLoginManager
-from flask_babel import Babel
from flask_principal import Principal
-from . import config_sql
-from . import logger
-from . import cache_buster
from .cli import CliParameter
from .constants import CONFIG_DIR
-from . import ub, db
from .reverseproxy import ReverseProxied
from .server import WebServer
from .dep_check import dependency_check
from . import services
from .updater import Updater
-
-try:
- import lxml
- lxml_present = True
-except ImportError:
- lxml_present = False
+from .babel import babel, BABEL_TRANSLATIONS
+from . import config_sql
+from . import logger
+from . import cache_buster
+from . import ub, db
try:
from flask_wtf.csrf import CSRFProtect
@@ -78,6 +68,8 @@ mimetypes.add_type('application/ogg', '.oga')
mimetypes.add_type('text/css', '.css')
mimetypes.add_type('text/javascript; charset=UTF-8', '.js')
+log = logger.create()
+
app = Flask(__name__)
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
@@ -86,14 +78,8 @@ app.config.update(
WTF_CSRF_SSL_STRICT=False
)
-
lm = MyLoginManager()
-babel = Babel()
-_BABEL_TRANSLATIONS = set()
-
-log = logger.create()
-
config = config_sql._ConfigSQL()
cli_param = CliParameter()
@@ -120,9 +106,8 @@ def create_app():
cli_param.init()
- ub.init_db(os.path.join(CONFIG_DIR, "app.db"), cli_param.user_credentials)
+ ub.init_db(cli_param.settings_path, cli_param.user_credentials)
- # ub.init_db(os.path.join(CONFIG_DIR, "app.db"))
# pylint: disable=no-member
config_sql.load_configuration(config, ub.session, cli_param)
@@ -139,26 +124,26 @@ def create_app():
if sys.version_info < (3, 0):
log.info(
- '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
+ '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
+ 'please update your installation to Python3 ***')
print(
- '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
+ '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
+ 'please update your installation to Python3 ***')
web_server.stop(True)
sys.exit(5)
- if not lxml_present:
- log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
- print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
- web_server.stop(True)
- sys.exit(6)
if not wtf_present:
- log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
- print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
+ log.info('*** "flask-WTF" is needed for calibre-web to run. '
+ 'Please install it using pip: "pip install flask-WTF" ***')
+ print('*** "flask-WTF" is needed for calibre-web to run. '
+ 'Please install it using pip: "pip install flask-WTF" ***')
web_server.stop(True)
sys.exit(7)
for res in dependency_check() + dependency_check(True):
- log.info('*** "{}" version does not fit the requirements. Should: {}, Found: {}, please consider installing required version ***'
- .format(res['name'],
- res['target'],
- res['found']))
+ log.info('*** "{}" version does not fit the requirements. '
+ 'Should: {}, Found: {}, please consider installing required version ***'
+ .format(res['name'],
+ res['target'],
+ res['found']))
app.wsgi_app = ReverseProxied(app.wsgi_app)
if os.environ.get('FLASK_DEBUG'):
@@ -172,8 +157,8 @@ def create_app():
web_server.init_app(app, config)
babel.init_app(app)
- _BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())
- _BABEL_TRANSLATIONS.add('en')
+ BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())
+ BABEL_TRANSLATIONS.add('en')
if services.ldap:
services.ldap.init_app(app, config)
@@ -185,27 +170,3 @@ def create_app():
return app
-@babel.localeselector
-def get_locale():
- # if a user is logged in, use the locale from the user settings
- user = getattr(g, 'user', None)
- if user is not None and hasattr(user, "locale"):
- if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
- return user.locale
-
- preferred = list()
- if request.accept_languages:
- for x in request.accept_languages.values():
- try:
- preferred.append(str(LC.parse(x.replace('-', '_'))))
- except (UnknownLocaleError, ValueError) as e:
- log.debug('Could not parse locale "%s": %s', x, e)
-
- return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
-
-
-'''@babel.timezoneselector
-def get_timezone():
- user = getattr(g, 'user', None)
- return user.timezone if user else None'''
-
diff --git a/cps/about.py b/cps/about.py
index 92dc41aa..1b68818d 100644
--- a/cps/about.py
+++ b/cps/about.py
@@ -65,7 +65,7 @@ _VERSIONS = OrderedDict(
SQLite=sqlite3.sqlite_version,
)
_VERSIONS.update(ret)
-_VERSIONS.update(uploader.get_versions(False))
+_VERSIONS.update(uploader.get_versions())
def collect_stats():
diff --git a/cps/admin.py b/cps/admin.py
index 46da062e..29f2319e 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -28,11 +28,10 @@ import operator
from datetime import datetime, timedelta, time
from functools import wraps
-from babel import Locale
-from babel.dates import format_datetime, format_time, format_timedelta
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _
+from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from flask import session as flask_session
from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified
@@ -40,14 +39,14 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services, cli
-from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, \
+from . import db, calibre_db, ub, web_server, config, updater_thread, babel, 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, update_thumbnail_cache
from .gdriveutils import is_gdrive_ready, gdrive_support
from .render_template import render_title_template, get_sidebar_config
from .services.worker import WorkerThread
-from . import debug_info, _BABEL_TRANSLATIONS
+from . import debug_info, BABEL_TRANSLATIONS
log = logger.create()
@@ -205,9 +204,9 @@ def admin():
all_user = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings()
- schedule_time = format_time(time(hour=config.schedule_start_time), format="short", locale=locale)
+ schedule_time = format_time(time(hour=config.schedule_start_time), format="short")
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
- schedule_duration = format_timedelta(t, format="short", threshold=.99, locale=locale)
+ schedule_duration = format_timedelta(t, threshold=.99)
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
feature_support=feature_support, schedule_time=schedule_time,
@@ -279,7 +278,7 @@ def view_configuration():
def edit_user_table():
visibility = current_user.view_settings.get('useredit', {})
languages = calibre_db.speaking_language()
- translations = babel.list_translations() + [Locale('en')]
+ translations = [LC('en')] + babel.list_translations()
all_user = ub.session.query(ub.User)
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
@@ -398,7 +397,7 @@ def delete_user():
@login_required
@admin_required
def table_get_locale():
- locale = babel.list_translations() + [Locale('en')]
+ locale = [LC('en')] + babel.list_translations()
ret = list()
current_locale = get_locale()
for loc in locale:
@@ -499,7 +498,7 @@ def edit_list_user(param):
elif param == 'locale':
if user.name == "Guest":
raise Exception(_("Guest's Locale is determined automatically and can't be set"))
- if vals['value'] in _BABEL_TRANSLATIONS:
+ if vals['value'] in BABEL_TRANSLATIONS:
user.locale = vals['value']
else:
raise Exception(_("No Valid Locale Given"))
@@ -1668,12 +1667,11 @@ def edit_scheduledtasks():
time_field = list()
duration_field = list()
- locale = get_locale()
for n in range(24):
- time_field.append((n , format_time(time(hour=n), format="short", locale=locale)))
+ time_field.append((n , format_time(time(hour=n), format="short",)))
for n in range(5, 65, 5):
t = timedelta(hours=n // 60, minutes=n % 60)
- duration_field.append((n, format_timedelta(t, format="short", threshold=.99, locale=locale)))
+ duration_field.append((n, format_timedelta(t, threshold=.9)))
return render_title_template("schedule_edit.html", config=content, starttime=time_field, duration=duration_field, title=_(u"Edit Scheduled Tasks Settings"))
diff --git a/cps/babel.py b/cps/babel.py
new file mode 100644
index 00000000..b0d5c238
--- /dev/null
+++ b/cps/babel.py
@@ -0,0 +1,30 @@
+from babel import Locale as LC
+from babel import negotiate_locale
+from flask_babel import Babel
+from babel.core import UnknownLocaleError
+from flask import request, g
+
+from . import logger
+
+log = logger.create()
+
+babel = Babel()
+BABEL_TRANSLATIONS = set()
+
+@babel.localeselector
+def get_locale():
+ # if a user is logged in, use the locale from the user settings
+ user = getattr(g, 'user', None)
+ if user is not None and hasattr(user, "locale"):
+ if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
+ return user.locale
+
+ preferred = list()
+ if request.accept_languages:
+ for x in request.accept_languages.values():
+ try:
+ preferred.append(str(LC.parse(x.replace('-', '_'))))
+ except (UnknownLocaleError, ValueError) as e:
+ log.debug('Could not parse locale "%s": %s', x, e)
+
+ return negotiate_locale(preferred or ['en'], BABEL_TRANSLATIONS)
diff --git a/cps/db.py b/cps/db.py
index 69796c51..f28baeca 100644
--- a/cps/db.py
+++ b/cps/db.py
@@ -43,6 +43,7 @@ from sqlalchemy.sql.expression import and_, true, false, text, func, or_
from sqlalchemy.ext.associationproxy import association_proxy
from flask_login import current_user
from flask_babel import gettext as _
+from flask_babel import get_locale
from flask import flash
from . import logger, ub, isoLanguages
@@ -898,7 +899,6 @@ class CalibreDB:
# Creates for all stored languages a translated speaking name in the array for the UI
def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False):
- from . import get_locale
if with_count:
if not languages:
diff --git a/cps/editbooks.py b/cps/editbooks.py
index 9ac1557b..3ac3dfb8 100755
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -36,11 +36,12 @@ except ImportError:
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
+from flask_babel import get_locale
from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError
# from sqlite3 import OperationalError as sqliteOperationalError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
-from . import config, get_locale, ub, db
+from . import config, ub, db
from . import calibre_db
from .services.worker import WorkerThread
from .tasks.upload import TaskUpload
diff --git a/cps/error_handler.py b/cps/error_handler.py
index 67252a66..7c003bdb 100644
--- a/cps/error_handler.py
+++ b/cps/error_handler.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
import traceback
+
from flask import render_template
from werkzeug.exceptions import default_exceptions
try:
diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py
index 9990e3db..e2e0a536 100644
--- a/cps/gdriveutils.py
+++ b/cps/gdriveutils.py
@@ -680,8 +680,3 @@ def get_error_text(client_secrets=None):
return 'Callback url (redirect url) is missing in client_secrets.json'
if client_secrets:
client_secrets.update(filedata['web'])
-
-
-def get_versions():
- return { # 'six': six_version,
- 'httplib2': httplib2_version}
diff --git a/cps/helper.py b/cps/helper.py
index d97d6475..aec14668 100644
--- a/cps/helper.py
+++ b/cps/helper.py
@@ -29,11 +29,11 @@ from tempfile import gettempdir
import requests
import unidecode
-from babel.dates import format_datetime
-from babel.units import format_unit
+
from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
+from flask_babel import format_datetime, get_locale
from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
from sqlalchemy.exc import InvalidRequestError, OperationalError
@@ -54,7 +54,7 @@ except ImportError:
from . import calibre_db, cli
from .tasks.convert import TaskConvert
-from . import logger, config, get_locale, db, ub, fs
+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
from .subproc_wrapper import process_wait
@@ -970,64 +970,6 @@ def json_serial(obj):
raise TypeError("Type %s not serializable" % type(obj))
-# helper function for displaying the runtime of tasks
-def format_runtime(runtime):
- ret_val = ""
- if runtime.days:
- ret_val = format_unit(runtime.days, 'duration-day', length="long", locale=get_locale()) + ', '
- mins, seconds = divmod(runtime.seconds, 60)
- hours, minutes = divmod(mins, 60)
- # ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ?
- if hours:
- ret_val += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds)
- elif minutes:
- ret_val += '{:2d}:{:02d}s'.format(minutes, seconds)
- else:
- ret_val += '{:2d}s'.format(seconds)
- return ret_val
-
-
-# helper function to apply localize status information in tasklist entries
-def render_task_status(tasklist):
- renderedtasklist = list()
- for __, user, __, task, __ in tasklist:
- if user == current_user.name or current_user.role_admin():
- ret = {}
- if task.start_time:
- ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
- ret['runtime'] = format_runtime(task.runtime)
-
- # localize the task status
- if isinstance(task.stat, int):
- if task.stat == STAT_WAITING:
- ret['status'] = _(u'Waiting')
- elif task.stat == STAT_FAIL:
- ret['status'] = _(u'Failed')
- elif task.stat == STAT_STARTED:
- ret['status'] = _(u'Started')
- elif task.stat == STAT_FINISH_SUCCESS:
- ret['status'] = _(u'Finished')
- elif task.stat == STAT_ENDED:
- ret['status'] = _(u'Ended')
- elif task.stat == STAT_CANCELLED:
- ret['status'] = _(u'Cancelled')
- else:
- ret['status'] = _(u'Unknown Status')
-
- ret['taskMessage'] = "{}: {}".format(task.name, task.message) if task.message else task.name
- ret['progress'] = "{} %".format(int(task.progress * 100))
- ret['user'] = escape(user) # prevent xss
-
- # Hidden fields
- ret['task_id'] = task.id
- ret['stat'] = task.stat
- ret['is_cancellable'] = task.is_cancellable
-
- renderedtasklist.append(ret)
-
- return renderedtasklist
-
-
def tags_filters():
negtags_list = current_user.list_denied_tags()
postags_list = current_user.list_allowed_tags()
diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py
index 50447aca..31e3dade 100644
--- a/cps/isoLanguages.py
+++ b/cps/isoLanguages.py
@@ -49,7 +49,7 @@ except ImportError:
def get_language_names(locale):
- return _LANGUAGE_NAMES.get(locale)
+ return _LANGUAGE_NAMES.get(str(locale))
def get_language_name(locale, lang_code):
diff --git a/cps/main.py b/cps/main.py
index b960028e..304a244a 100644
--- a/cps/main.py
+++ b/cps/main.py
@@ -24,6 +24,7 @@ from .shelf import shelf
from .remotelogin import remotelogin
from .search_metadata import meta
from .error_handler import init_errorhandler
+from .tasks_status import tasks
try:
from kobo import kobo, get_kobo_activated
@@ -48,16 +49,19 @@ def main():
from .gdrive import gdrive
from .editbooks import editbook
from .about import about
+ from .search import search
from . import web_server
init_errorhandler()
+ app.register_blueprint(search)
+ app.register_blueprint(tasks)
app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
- app.register_blueprint(admi) #
+ app.register_blueprint(admi)
app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive)
diff --git a/cps/oauth.py b/cps/oauth.py
index f8e5c1fd..0caa61ec 100644
--- a/cps/oauth.py
+++ b/cps/oauth.py
@@ -19,18 +19,12 @@
from flask import session
try:
- from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
+ from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
+ from flask_dance.consumer.storage.sqla import first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound
- backend_resultcode = False # prevent storing values with this resultcode
+ backend_resultcode = True # prevent storing values with this resultcode
except ImportError:
- # fails on flask-dance >1.3, due to renaming
- try:
- from flask_dance.consumer.storage.sqla import SQLAlchemyStorage as SQLAlchemyBackend
- from flask_dance.consumer.storage.sqla import first, _get_real_user
- from sqlalchemy.orm.exc import NoResultFound
- backend_resultcode = True # prevent storing values with this resultcode
- except ImportError:
- pass
+ pass
class OAuthBackend(SQLAlchemyBackend):
diff --git a/cps/opds.py b/cps/opds.py
index cb8f397e..2b8ab6d6 100644
--- a/cps/opds.py
+++ b/cps/opds.py
@@ -26,15 +26,18 @@ from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask_login import current_user
+from flask_babel import get_locale
from sqlalchemy.sql.expression import func, text, or_, and_, true
from sqlalchemy.exc import InvalidRequestError, OperationalError
from werkzeug.security import check_password_hash
-from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
+
+from . import constants, logger, config, db, calibre_db, ub, services, isoLanguages
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
from .web import render_read_books
from .usermanagement import load_user_from_request
from flask_babel import gettext as _
+
opds = Blueprint('opds', __name__)
log = logger.create()
diff --git a/cps/redirect.py b/cps/redirect.py
index 8bd68109..9382a205 100644
--- a/cps/redirect.py
+++ b/cps/redirect.py
@@ -29,7 +29,6 @@
from urllib.parse import urlparse, urljoin
-
from flask import request, url_for, redirect
diff --git a/cps/render_template.py b/cps/render_template.py
index d2f40d6c..0750a9c4 100644
--- a/cps/render_template.py
+++ b/cps/render_template.py
@@ -16,9 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from flask import render_template, request
+from flask import render_template, g, abort, request
from flask_babel import gettext as _
-from flask import g, abort
from werkzeug.local import LocalProxy
from flask_login import current_user
diff --git a/cps/search.py b/cps/search.py
new file mode 100644
index 00000000..429aea17
--- /dev/null
+++ b/cps/search.py
@@ -0,0 +1,422 @@
+# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
+# Copyright (C) 2022 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 .
+
+import json
+from datetime import datetime
+
+from flask import Blueprint, request, redirect, url_for, flash
+from flask import session as flask_session
+from flask_login import current_user
+from flask_babel import get_locale, format_date
+from flask_babel import gettext as _
+from sqlalchemy.sql.expression import func, not_, and_, or_, text
+from sqlalchemy.sql.functions import coalesce
+
+from . import logger, db, calibre_db, config, ub
+from .usermanagement import login_required_if_no_ano
+from .render_template import render_title_template
+from .pagination import Pagination
+
+search = Blueprint('search', __name__)
+
+log = logger.create()
+
+
+@search.route("/search", methods=["GET"])
+@login_required_if_no_ano
+def simple_search():
+ term = request.args.get("query")
+ if term:
+ return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
+ else:
+ return render_title_template('search.html',
+ searchterm="",
+ result_count=0,
+ title=_(u"Search"),
+ page="search")
+
+
+@search.route("/advsearch", methods=['POST'])
+@login_required_if_no_ano
+def advanced_search():
+ values = dict(request.form)
+ params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf', 'exclude_shelf',
+ 'include_language', 'exclude_language', 'include_extension', 'exclude_extension']
+ for param in params:
+ values[param] = list(request.form.getlist(param))
+ flask_session['query'] = json.dumps(values)
+ return redirect(url_for('web.books_list', data="advsearch", sort_param='stored', query=""))
+
+
+@search.route("/advsearch", methods=['GET'])
+@login_required_if_no_ano
+def advanced_search_form():
+ # Build custom columns names
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
+ return render_prepare_search_form(cc)
+
+
+def adv_search_custom_columns(cc, term, q):
+ for c in cc:
+ if c.datatype == "datetime":
+ custom_start = term.get('custom_column_' + str(c.id) + '_start')
+ custom_end = term.get('custom_column_' + str(c.id) + '_end')
+ if custom_start:
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ func.datetime(db.cc_classes[c.id].value) >= func.datetime(custom_start)))
+ if custom_end:
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
+ else:
+ custom_query = term.get('custom_column_' + str(c.id))
+ if custom_query != '' and custom_query is not None:
+ if c.datatype == 'bool':
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ db.cc_classes[c.id].value == (custom_query == "True")))
+ elif c.datatype == 'int' or c.datatype == 'float':
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ db.cc_classes[c.id].value == custom_query))
+ elif c.datatype == 'rating':
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ db.cc_classes[c.id].value == int(float(custom_query) * 2)))
+ else:
+ q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
+ func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
+ return q
+
+
+def adv_search_language(q, include_languages_inputs, exclude_languages_inputs):
+ if current_user.filter_language() != "all":
+ q = q.filter(db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()))
+ else:
+ for language in include_languages_inputs:
+ q = q.filter(db.Books.languages.any(db.Languages.id == language))
+ for language in exclude_languages_inputs:
+ q = q.filter(not_(db.Books.series.any(db.Languages.id == language)))
+ return q
+
+
+def adv_search_ratings(q, rating_high, rating_low):
+ if rating_high:
+ rating_high = int(rating_high) * 2
+ q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high))
+ if rating_low:
+ rating_low = int(rating_low) * 2
+ q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low))
+ return q
+
+
+def adv_search_read_status(q, read_status):
+ if read_status:
+ if config.config_read_column:
+ try:
+ if read_status == "True":
+ q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
+ .filter(db.cc_classes[config.config_read_column].value == True)
+ else:
+ q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
+ .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
+ except (KeyError, AttributeError):
+ log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column)
+ flash(_("Custom Column No.%(column)d is not existing in calibre database",
+ column=config.config_read_column),
+ category="error")
+ return q
+ else:
+ if read_status == "True":
+ q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
+ .filter(ub.ReadBook.user_id == int(current_user.id),
+ ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
+ else:
+ q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
+ .filter(ub.ReadBook.user_id == int(current_user.id),
+ coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED)
+ return q
+
+
+def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
+ for extension in include_extension_inputs:
+ q = q.filter(db.Books.data.any(db.Data.format == extension))
+ for extension in exclude_extension_inputs:
+ q = q.filter(not_(db.Books.data.any(db.Data.format == extension)))
+ return q
+
+
+def adv_search_tag(q, include_tag_inputs, exclude_tag_inputs):
+ for tag in include_tag_inputs:
+ q = q.filter(db.Books.tags.any(db.Tags.id == tag))
+ for tag in exclude_tag_inputs:
+ q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
+ return q
+
+
+def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
+ for serie in include_series_inputs:
+ q = q.filter(db.Books.series.any(db.Series.id == serie))
+ for serie in exclude_series_inputs:
+ q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
+ return q
+
+def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
+ q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\
+ .filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
+ if len(include_shelf_inputs) > 0:
+ q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
+ return q
+
+def extend_search_term(searchterm,
+ author_name,
+ book_title,
+ publisher,
+ pub_start,
+ pub_end,
+ tags,
+ rating_high,
+ rating_low,
+ read_status,
+ ):
+ searchterm.extend((author_name.replace('|', ','), book_title, publisher))
+ if pub_start:
+ try:
+ searchterm.extend([_(u"Published after ") +
+ format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
+ format='medium', locale=get_locale())])
+ except ValueError:
+ pub_start = u""
+ if pub_end:
+ try:
+ searchterm.extend([_(u"Published before ") +
+ format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
+ format='medium', locale=get_locale())])
+ except ValueError:
+ pub_end = u""
+ elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
+ for key, db_element in elements.items():
+ tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
+ searchterm.extend(tag.name for tag in tag_names)
+ tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['exclude_' + key])).all()
+ searchterm.extend(tag.name for tag in tag_names)
+ language_names = calibre_db.session.query(db.Languages). \
+ filter(db.Languages.id.in_(tags['include_language'])).all()
+ if language_names:
+ language_names = calibre_db.speaking_language(language_names)
+ searchterm.extend(language.name for language in language_names)
+ language_names = calibre_db.session.query(db.Languages). \
+ filter(db.Languages.id.in_(tags['exclude_language'])).all()
+ if language_names:
+ language_names = calibre_db.speaking_language(language_names)
+ searchterm.extend(language.name for language in language_names)
+ if rating_high:
+ searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
+ if rating_low:
+ searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
+ if read_status:
+ searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
+ searchterm.extend(ext for ext in tags['include_extension'])
+ searchterm.extend(ext for ext in tags['exclude_extension'])
+ # handle custom columns
+ searchterm = " + ".join(filter(None, searchterm))
+ return searchterm, pub_start, pub_end
+
+
+def render_adv_search_results(term, offset=None, order=None, limit=None):
+ sort = order[0] if order else [db.Books.sort]
+ pagination = None
+
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
+ calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
+ if not config.config_read_column:
+ query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, ub.ReadBook).select_from(db.Books)
+ .outerjoin(ub.ReadBook, and_(db.Books.id == ub.ReadBook.book_id,
+ int(current_user.id) == ub.ReadBook.user_id)))
+ else:
+ try:
+ read_column = cc[config.config_read_column]
+ query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, read_column.value)
+ .select_from(db.Books)
+ .outerjoin(read_column, read_column.book == db.Books.id))
+ except (KeyError, AttributeError):
+ log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
+ # Skip linking read column
+ query = calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, None)
+ query = query.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
+ int(current_user.id) == ub.ArchivedBook.user_id))
+
+ q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
+ .outerjoin(db.Series)\
+ .filter(calibre_db.common_filters(True))
+
+ # parse multi selects to a complete dict
+ tags = dict()
+ elements = ['tag', 'serie', 'shelf', 'language', 'extension']
+ for element in elements:
+ tags['include_' + element] = term.get('include_' + element)
+ tags['exclude_' + element] = term.get('exclude_' + element)
+
+ author_name = term.get("author_name")
+ book_title = term.get("book_title")
+ publisher = term.get("publisher")
+ pub_start = term.get("publishstart")
+ pub_end = term.get("publishend")
+ rating_low = term.get("ratinghigh")
+ rating_high = term.get("ratinglow")
+ description = term.get("comment")
+ read_status = term.get("read_status")
+ if author_name:
+ author_name = author_name.strip().lower().replace(',', '|')
+ if book_title:
+ book_title = book_title.strip().lower()
+ if publisher:
+ publisher = publisher.strip().lower()
+
+ search_term = []
+ cc_present = False
+ for c in cc:
+ if c.datatype == "datetime":
+ column_start = term.get('custom_column_' + str(c.id) + '_start')
+ column_end = term.get('custom_column_' + str(c.id) + '_end')
+ if column_start:
+ search_term.extend([u"{} >= {}".format(c.name,
+ format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
+ format='medium',
+ locale=get_locale())
+ )])
+ cc_present = True
+ if column_end:
+ search_term.extend([u"{} <= {}".format(c.name,
+ format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
+ format='medium',
+ locale=get_locale())
+ )])
+ cc_present = True
+ elif term.get('custom_column_' + str(c.id)):
+ search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
+ cc_present = True
+
+
+ if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
+ or rating_high or description or cc_present or read_status:
+ search_term, pub_start, pub_end = extend_search_term(search_term,
+ author_name,
+ book_title,
+ publisher,
+ pub_start,
+ pub_end,
+ tags,
+ rating_high,
+ rating_low,
+ read_status)
+ if author_name:
+ q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_name + "%")))
+ if book_title:
+ q = q.filter(func.lower(db.Books.title).ilike("%" + book_title + "%"))
+ if pub_start:
+ q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
+ if pub_end:
+ q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
+ q = adv_search_read_status(q, read_status)
+ if publisher:
+ q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
+ q = adv_search_tag(q, tags['include_tag'], tags['exclude_tag'])
+ q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie'])
+ q = adv_search_shelf(q, tags['include_shelf'], tags['exclude_shelf'])
+ q = adv_search_extension(q, tags['include_extension'], tags['exclude_extension'])
+ q = adv_search_language(q, tags['include_language'], tags['exclude_language'])
+ q = adv_search_ratings(q, rating_high, rating_low)
+
+ if description:
+ q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
+
+ # search custom columns
+ try:
+ q = adv_search_custom_columns(cc, term, q)
+ except AttributeError as ex:
+ log.debug_or_exception(ex)
+ flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error")
+
+ q = q.order_by(*sort).all()
+ flask_session['query'] = json.dumps(term)
+ ub.store_combo_ids(q)
+ result_count = len(q)
+ if offset is not None and limit is not None:
+ offset = int(offset)
+ limit_all = offset + int(limit)
+ pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
+ else:
+ offset = 0
+ limit_all = result_count
+ entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
+ return render_title_template('search.html',
+ adv_searchterm=search_term,
+ pagination=pagination,
+ entries=entries,
+ result_count=result_count,
+ title=_(u"Advanced Search"), page="advsearch",
+ order=order[1])
+
+
+def render_prepare_search_form(cc):
+ # prepare data for search-form
+ tags = calibre_db.session.query(db.Tags)\
+ .join(db.books_tags_link)\
+ .join(db.Books)\
+ .filter(calibre_db.common_filters()) \
+ .group_by(text('books_tags_link.tag'))\
+ .order_by(db.Tags.name).all()
+ series = calibre_db.session.query(db.Series)\
+ .join(db.books_series_link)\
+ .join(db.Books)\
+ .filter(calibre_db.common_filters()) \
+ .group_by(text('books_series_link.series'))\
+ .order_by(db.Series.name)\
+ .filter(calibre_db.common_filters()).all()
+ shelves = ub.session.query(ub.Shelf)\
+ .filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\
+ .order_by(ub.Shelf.name).all()
+ extensions = calibre_db.session.query(db.Data)\
+ .join(db.Books)\
+ .filter(calibre_db.common_filters()) \
+ .group_by(db.Data.format)\
+ .order_by(db.Data.format).all()
+ if current_user.filter_language() == u"all":
+ languages = calibre_db.speaking_language()
+ else:
+ languages = None
+ return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
+ series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
+
+
+def render_search_results(term, offset=None, order=None, limit=None):
+ join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
+ entries, result_count, pagination = calibre_db.get_search_results(term,
+ config,
+ offset,
+ order,
+ limit,
+ False,
+ *join)
+ return render_title_template('search.html',
+ searchterm=term,
+ pagination=pagination,
+ query=term,
+ adv_searchterm=term,
+ entries=entries,
+ result_count=result_count,
+ title=_(u"Search"),
+ page="search",
+ order=order[1])
+
+
diff --git a/cps/search_metadata.py b/cps/search_metadata.py
index 0070e78f..ae95a28e 100644
--- a/cps/search_metadata.py
+++ b/cps/search_metadata.py
@@ -22,17 +22,17 @@ import inspect
import json
import os
import sys
-# from time import time
-
+from dataclasses import asdict
from flask import Blueprint, Response, request, url_for
from flask_login import current_user
from flask_login import login_required
+from flask_babel import get_locale
from sqlalchemy.exc import InvalidRequestError, OperationalError
from sqlalchemy.orm.attributes import flag_modified
from cps.services.Metadata import Metadata
-from . import constants, get_locale, logger, ub, web_server
+from . import constants, logger, ub, web_server
# current_milli_time = lambda: int(round(time() * 1000))
diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py
index e65d314a..bc7e928a 100644
--- a/cps/tasks/convert.py
+++ b/cps/tasks/convert.py
@@ -55,7 +55,8 @@ class TaskConvert(CalibreTask):
def run(self, worker_thread):
self.worker_thread = worker_thread
if config.config_use_google_drive:
- worker_db = db.CalibreDB(expire_on_commit=False)
+ worker_db = db.CalibreDB()
+ worker_db.init_db(expire_on_commit=False)
cur_book = worker_db.get_book(self.book_id)
self.title = cur_book.title
data = worker_db.get_book_format(self.book_id, self.settings['old_book_format'])
@@ -104,7 +105,8 @@ class TaskConvert(CalibreTask):
def _convert_ebook_format(self):
error_message = None
- local_db = db.CalibreDB(expire_on_commit=False)
+ local_db = db.CalibreDB()
+ local_db.init_db(expire_on_commit=False)
file_path = self.file_path
book_id = self.book_id
format_old_ext = u'.' + self.settings['old_book_format'].lower()
diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py
index dcfd4226..2ecc57c8 100644
--- a/cps/tasks/thumbnail.py
+++ b/cps/tasks/thumbnail.py
@@ -68,7 +68,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
self.log = logger.create()
self.book_id = book_id
self.app_db_session = ub.get_new_session_instance()
- self.calibre_db = db.CalibreDB(expire_on_commit=False)
+ self.calibre_db = db.CalibreDB()
+ self.calibre_db.init_db(expire_on_commit=False)
self.cache = fs.FileSystem()
self.resolutions = [
constants.COVER_THUMBNAIL_SMALL,
@@ -238,7 +239,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
super(TaskGenerateSeriesThumbnails, self).__init__(task_message)
self.log = logger.create()
self.app_db_session = ub.get_new_session_instance()
- self.calibre_db = db.CalibreDB(expire_on_commit=False)
+ self.calibre_db = db.CalibreDB()
+ self.calibre_db.init_db(expire_on_commit=False)
self.cache = fs.FileSystem()
self.resolutions = [
constants.COVER_THUMBNAIL_SMALL,
@@ -448,7 +450,8 @@ class TaskClearCoverThumbnailCache(CalibreTask):
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
self.log = logger.create()
self.book_id = book_id
- self.calibre_db = db.CalibreDB(expire_on_commit=False)
+ self.calibre_db = db.CalibreDB()
+ self.calibre_db.init_db(expire_on_commit=False)
self.app_db_session = ub.get_new_session_instance()
self.cache = fs.FileSystem()
diff --git a/cps/tasks_status.py b/cps/tasks_status.py
new file mode 100644
index 00000000..ca9b5796
--- /dev/null
+++ b/cps/tasks_status.py
@@ -0,0 +1,95 @@
+# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
+# Copyright (C) 2022 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 .
+
+from markupsafe import escape
+
+from flask import Blueprint, jsonify
+from flask_login import login_required, current_user
+from flask_babel import gettext as _
+from flask_babel import get_locale, format_datetime
+from babel.units import format_unit
+
+from . import logger
+from .render_template import render_title_template
+from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
+
+tasks = Blueprint('tasks', __name__)
+
+log = logger.create()
+
+
+@tasks.route("/ajax/emailstat")
+@login_required
+def get_email_status_json():
+ tasks = WorkerThread.getInstance().tasks
+ return jsonify(render_task_status(tasks))
+
+
+@tasks.route("/tasks")
+@login_required
+def get_tasks_status():
+ # if current user admin, show all email, otherwise only own emails
+ tasks = WorkerThread.getInstance().tasks
+ answer = render_task_status(tasks)
+ return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
+
+
+# helper function to apply localize status information in tasklist entries
+def render_task_status(tasklist):
+ rendered_tasklist = list()
+ for __, user, __, task in tasklist:
+ if user == current_user.name or current_user.role_admin():
+ ret = {}
+ if task.start_time:
+ ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
+ ret['runtime'] = format_runtime(task.runtime)
+
+ # localize the task status
+ if isinstance(task.stat, int):
+ if task.stat == STAT_WAITING:
+ ret['status'] = _(u'Waiting')
+ elif task.stat == STAT_FAIL:
+ ret['status'] = _(u'Failed')
+ elif task.stat == STAT_STARTED:
+ ret['status'] = _(u'Started')
+ elif task.stat == STAT_FINISH_SUCCESS:
+ ret['status'] = _(u'Finished')
+ else:
+ ret['status'] = _(u'Unknown Status')
+
+ ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
+ ret['progress'] = "{} %".format(int(task.progress * 100))
+ ret['user'] = escape(user) # prevent xss
+ rendered_tasklist.append(ret)
+
+ return rendered_tasklist
+
+
+# helper function for displaying the runtime of tasks
+def format_runtime(runtime):
+ ret_val = ""
+ if runtime.days:
+ ret_val = format_unit(runtime.days, 'duration-day', length="long", locale=get_locale()) + ', '
+ minutes, seconds = divmod(runtime.seconds, 60)
+ hours, minutes = divmod(minutes, 60)
+ # ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ?
+ if hours:
+ ret_val += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds)
+ elif minutes:
+ ret_val += '{:2d}:{:02d}s'.format(minutes, seconds)
+ else:
+ ret_val += '{:2d}s'.format(seconds)
+ return ret_val
diff --git a/cps/templates/layout.html b/cps/templates/layout.html
index 42012937..7502514a 100644
--- a/cps/templates/layout.html
+++ b/cps/templates/layout.html
@@ -41,7 +41,7 @@
{{instance}}
{% if g.user.is_authenticated or g.allow_anonymous %}
-