- {% for number in range((entry.ratings[0].rating/2)|int(2)) %}
+ {% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
diff --git a/cps/templates/shelfdown.html b/cps/templates/shelfdown.html
index c800dca7..f1a0b137 100644
--- a/cps/templates/shelfdown.html
+++ b/cps/templates/shelfdown.html
@@ -35,31 +35,31 @@
{% endif %}
{% endif %}
- {% if kobo_support and not content.role_anonymous() %}
+ {% if kobo_support and not content.role_anonymous() and not simple%}
{{_('Sync only books in selected shelves with Kobo')}}
diff --git a/cps/tornado_wsgi.py b/cps/tornado_wsgi.py
new file mode 100644
index 00000000..af93219c
--- /dev/null
+++ b/cps/tornado_wsgi.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+
+# 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 tornado.wsgi import WSGIContainer
+import tornado
+
+from tornado import escape
+from tornado import httputil
+
+from typing import List, Tuple, Optional, Callable, Any, Dict, Text
+from types import TracebackType
+import typing
+
+if typing.TYPE_CHECKING:
+ from typing import Type # noqa: F401
+ from wsgiref.types import WSGIApplication as WSGIAppType # noqa: F4
+
+class MyWSGIContainer(WSGIContainer):
+
+ def __call__(self, request: httputil.HTTPServerRequest) -> None:
+ data = {} # type: Dict[str, Any]
+ response = [] # type: List[bytes]
+
+ def start_response(
+ status: str,
+ headers: List[Tuple[str, str]],
+ exc_info: Optional[
+ Tuple[
+ "Optional[Type[BaseException]]",
+ Optional[BaseException],
+ Optional[TracebackType],
+ ]
+ ] = None,
+ ) -> Callable[[bytes], Any]:
+ data["status"] = status
+ data["headers"] = headers
+ return response.append
+
+ app_response = self.wsgi_application(
+ MyWSGIContainer.environ(request), start_response
+ )
+ try:
+ response.extend(app_response)
+ body = b"".join(response)
+ finally:
+ if hasattr(app_response, "close"):
+ app_response.close() # type: ignore
+ if not data:
+ raise Exception("WSGI app did not call start_response")
+
+ status_code_str, reason = data["status"].split(" ", 1)
+ status_code = int(status_code_str)
+ headers = data["headers"] # type: List[Tuple[str, str]]
+ header_set = set(k.lower() for (k, v) in headers)
+ body = escape.utf8(body)
+ if status_code != 304:
+ if "content-length" not in header_set:
+ headers.append(("Content-Length", str(len(body))))
+ if "content-type" not in header_set:
+ headers.append(("Content-Type", "text/html; charset=UTF-8"))
+ if "server" not in header_set:
+ headers.append(("Server", "TornadoServer/%s" % tornado.version))
+
+ start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
+ header_obj = httputil.HTTPHeaders()
+ for key, value in headers:
+ header_obj.add(key, value)
+ assert request.connection is not None
+ request.connection.write_headers(start_line, header_obj, chunk=body)
+ request.connection.finish()
+ self._log(status_code, request)
+
+ @staticmethod
+ def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
+ environ = WSGIContainer.environ(request)
+ environ['RAW_URI'] = request.path
+ return environ
+
diff --git a/cps/web.py b/cps/web.py
index babc3bcc..bc05ec66 100644
--- a/cps/web.py
+++ b/cps/web.py
@@ -49,7 +49,7 @@ from . import constants, logger, isoLanguages, services
from . import babel, db, ub, config, get_locale, app
from . import calibre_db, kobo_sync_status
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
-from .helper import check_valid_domain, render_task_status, check_email, check_username, get_cc_columns, \
+from .helper import check_valid_domain, render_task_status, check_email, check_username, \
get_book_cover, get_series_cover_thumbnail, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email, \
edit_book_read_status
@@ -85,7 +85,10 @@ except ImportError:
def add_security_headers(resp):
csp = "default-src 'self'"
csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')])
- csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:"
+ csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' "
+ if request.path.startswith("/author/") and config.config_use_goodreads:
+ csp += "images.gr-assets.com i.gr-assets.com s.gr-assets.com"
+ csp += " data:"
resp.headers['Content-Security-Policy'] = csp
if request.endpoint == "edit-book.show_edit_book" or config.config_use_google_drive:
resp.headers['Content-Security-Policy'] += " *"
@@ -350,7 +353,7 @@ def render_books_list(data, sort_param, book_id, page):
if data == "rated":
return render_rated_books(page, book_id, order=order)
elif data == "discover":
- return render_discover_books(page, book_id)
+ return render_discover_books(book_id)
elif data == "unread":
return render_read_books(page, False, order=order)
elif data == "read":
@@ -386,7 +389,7 @@ def render_books_list(data, sort_param, book_id, page):
else:
website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -400,7 +403,7 @@ def render_rated_books(page, book_id, order):
db.Books,
db.Books.ratings.any(db.Ratings.rating > 9),
order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -411,11 +414,13 @@ def render_rated_books(page, book_id, order):
abort(404)
-def render_discover_books(page, book_id):
+def render_discover_books(book_id):
if current_user.check_visibility(constants.SIDEBAR_RANDOM):
- entries, __, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, [func.randomblob(2)])
+ entries, __, ___ = calibre_db.fill_indexpage(1, 0, db.Books, True, [func.randomblob(2)],
+ join_archive_read=True,
+ config_read_column=config.config_read_column)
pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page)
- return render_title_template('discover.html', entries=entries, pagination=pagination, id=book_id,
+ return render_title_template('index.html', random=false(), entries=entries, pagination=pagination, id=book_id,
title=_(u"Discover (Random Books)"), page="discover")
else:
abort(404)
@@ -429,18 +434,22 @@ def render_hot_books(page, order):
# order[0][0].compare(func.count(ub.Downloads.book_id).asc())):
order = [func.count(ub.Downloads.book_id).desc()], 'hotdesc'
if current_user.show_detail_random():
- random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
- .order_by(func.random()).limit(config.config_random_books)
+ random_query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ random = (random_query.filter(calibre_db.common_filters())
+ .order_by(func.random())
+ .limit(config.config_random_books).all())
else:
random = false()
+
off = int(int(config.config_books_per_page) * (page - 1))
all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)) \
.order_by(*order[0]).group_by(ub.Downloads.book_id)
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
- download_book = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).filter(
- db.Books.id == book.Downloads.book_id).first()
+ query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ download_book = query.filter(calibre_db.common_filters()).filter(
+ book.Downloads.book_id == db.Books.id).first()
if download_book:
entries.append(download_book)
else:
@@ -459,26 +468,20 @@ def render_downloaded_books(page, order, user_id):
else:
user_id = current_user.id
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
- if current_user.show_detail_random():
- random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
- .order_by(func.random()).limit(config.config_random_books)
- else:
- random = false()
-
- entries, __, pagination = calibre_db.fill_indexpage(page,
+ entries, random, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == user_id,
order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series,
ub.Downloads, db.Books.id == ub.Downloads.book_id)
for book in entries:
- if not calibre_db.session.query(db.Books).\
- filter(calibre_db.common_filters()).filter(db.Books.id == book.id).first():
- ub.delete_download(book.id)
+ if not (calibre_db.session.query(db.Books).filter(calibre_db.common_filters())
+ .filter(db.Books.id == book.Books.id).first()):
+ ub.delete_download(book.Books.id)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html',
random=random,
@@ -497,9 +500,9 @@ def render_author_books(page, author_id, order):
db.Books,
db.Books.authors.any(db.Authors.id == author_id),
[order[0][0], db.Series.name, db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
+ db.books_series_link.c.book == db.Books.id,
db.Series)
if entries is None or not len(entries):
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
@@ -515,7 +518,8 @@ def render_author_books(page, author_id, order):
other_books = []
if services.goodreads_support and config.config_use_goodreads:
author_info = services.goodreads_support.get_author_info(author_name)
- other_books = services.goodreads_support.get_other_books(author_info, entries)
+ book_entries = [entry.Books for entry in entries]
+ other_books = services.goodreads_support.get_other_books(author_info, book_entries)
return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id,
title=_(u"Author: %(name)s", name=author_name), author=author_info,
other_books=other_books, page="author", order=order[1])
@@ -528,7 +532,7 @@ def render_publisher_books(page, book_id, order):
db.Books,
db.Books.publishers.any(db.Publishers.id == book_id),
[db.Series.name, order[0][0], db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -546,7 +550,8 @@ def render_series_books(page, book_id, order):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.series.any(db.Series.id == book_id),
- [order[0][0]])
+ [order[0][0]],
+ True, config.config_read_column)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"Series: %(serie)s", serie=name.name), page="series", order=order[1])
else:
@@ -558,7 +563,8 @@ def render_ratings_books(page, book_id, order):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.ratings.any(db.Ratings.id == book_id),
- [order[0][0]])
+ [order[0][0]],
+ True, config.config_read_column)
if name and name.rating <= 10:
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)),
@@ -574,7 +580,8 @@ def render_formats_books(page, book_id, order):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.data.any(db.Data.format == book_id.upper()),
- [order[0][0]])
+ [order[0][0]],
+ True, config.config_read_column)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"File format: %(format)s", format=name.format),
page="formats",
@@ -590,7 +597,7 @@ def render_category_books(page, book_id, order):
db.Books,
db.Books.tags.any(db.Tags.id == book_id),
[order[0][0], db.Series.name, db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -609,7 +616,8 @@ def render_language_books(page, name, order):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.languages.any(db.Languages.lang_code == name),
- [order[0][0]])
+ [order[0][0]],
+ True, config.config_read_column)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language", order=order[1])
@@ -622,30 +630,12 @@ def render_read_books(page, are_read, as_xml=False, order=None):
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db_filter,
- sort_param,
- False, 0,
- db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
- db.Series,
- ub.ReadBook, db.Books.id == ub.ReadBook.book_id)
else:
try:
if are_read:
db_filter = db.cc_classes[config.config_read_column].value == True
else:
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db_filter,
- sort_param,
- False, 0,
- db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
- db.Series,
- db.cc_classes[config.config_read_column])
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
if not as_xml:
@@ -655,6 +645,15 @@ def render_read_books(page, are_read, as_xml=False, order=None):
return redirect(url_for("web.index"))
return [] # ToDo: Handle error Case for opds
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db_filter,
+ sort_param,
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+
if as_xml:
return entries, pagination
else:
@@ -683,7 +682,7 @@ def render_archived_books(page, sort_param):
archived_filter,
order,
True,
- False, 0)
+ True, config.config_read_column)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
page_name = "archived"
@@ -723,12 +722,12 @@ def render_prepare_search_form(cc):
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
+ join = db.books_series_link, db.books_series_link.c.book == db.Books.id, db.Series
entries, result_count, pagination = calibre_db.get_search_results(term,
+ config,
offset,
order,
limit,
- config.config_read_column,
*join)
return render_title_template('search.html',
searchterm=term,
@@ -766,7 +765,7 @@ def books_list(data, sort_param, book_id, page):
@login_required
def books_table():
visibility = current_user.view_settings.get('table', {})
- cc = get_cc_columns(filter_config_custom_read=True)
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_title_template('book_table.html', title=_(u"Books List"), cc=cc, page="book_table",
visiblility=visibility)
@@ -810,37 +809,18 @@ def list_books():
calibre_db.common_filters(allow_show_archived=True)).count()
if state is not None:
if search_param:
- books = calibre_db.search_query(search_param, config.config_read_column).all()
+ books = calibre_db.search_query(search_param, config).all()
filtered_count = len(books)
else:
- if not config.config_read_column:
- books = (calibre_db.session.query(db.Books, ub.ReadBook.read_status, ub.ArchivedBook.is_archived)
- .select_from(db.Books)
- .outerjoin(ub.ReadBook,
- and_(ub.ReadBook.user_id == int(current_user.id),
- ub.ReadBook.book_id == db.Books.id)))
- else:
- read_column = ""
- try:
- read_column = db.cc_classes[config.config_read_column]
- books = (calibre_db.session.query(db.Books, read_column.value, ub.ArchivedBook.is_archived)
- .select_from(db.Books)
- .outerjoin(read_column, read_column.book == db.Books.id))
- except (KeyError, AttributeError, IndexError):
- log.error(
- "Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
- # Skip linking read column and return None instead of read status
- books = calibre_db.session.query(db.Books, None, ub.ArchivedBook.is_archived)
- books = (books.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
- int(current_user.id) == ub.ArchivedBook.user_id))
- .filter(calibre_db.common_filters(allow_show_archived=True)).all())
+ query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ books = query.filter(calibre_db.common_filters(allow_show_archived=True)).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order, True)
elif search_param:
entries, filtered_count, __ = calibre_db.get_search_results(search_param,
+ config,
off,
[order, ''],
limit,
- config.config_read_column,
*join)
else:
entries, __, __ = calibre_db.fill_indexpage_with_archived_books((int(off) / (int(limit)) + 1),
@@ -856,8 +836,8 @@ def list_books():
result = list()
for entry in entries:
val = entry[0]
- val.read_status = entry[1] == ub.ReadBook.STATUS_FINISHED
- val.is_archived = entry[2] is True
+ val.is_archived = entry[1] is True
+ val.read_status = entry[2] == ub.ReadBook.STATUS_FINISHED
for lang_index in range(0, len(val.languages)):
val.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[
lang_index].lang_code)
@@ -1252,26 +1232,10 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
sort_param = order[0] if order else [db.Books.sort]
pagination = None
- cc = get_cc_columns(filter_config_custom_read=True)
+ 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, IndexError):
- log.error("Custom Column No.{} is not existing in calibre database".format(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) \
+ query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ q = query.outerjoin(db.books_series_link, db.books_series_link.c.book == db.Books.id) \
.outerjoin(db.Series) \
.filter(calibre_db.common_filters(True))
@@ -1357,7 +1321,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
if description:
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
- # search custom culumns
+ # search custom columns
try:
q = adv_search_custom_columns(cc, term, q)
except AttributeError as ex:
@@ -1390,7 +1354,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
@login_required_if_no_ano
def advanced_search_form():
# Build custom columns names
- cc = get_cc_columns(filter_config_custom_read=True)
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_prepare_search_form(cc)
@@ -1800,10 +1764,10 @@ def show_book(book_id):
for lang_index in range(0, len(entry.languages)):
entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
lang_index].lang_code)
- cc = get_cc_columns(filter_config_custom_read=True)
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
book_in_shelves = []
- shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
- for sh in shelfs:
+ shelves = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
+ for sh in shelves:
book_in_shelves.append(sh.shelf)
entry.tags = sort(entry.tags, key=lambda tag: tag.name)
diff --git a/optional-requirements.txt b/optional-requirements.txt
index e54b0829..208d889d 100644
--- a/optional-requirements.txt
+++ b/optional-requirements.txt
@@ -1,5 +1,5 @@
# GDrive Integration
-google-api-python-client>=1.7.11,<2.42.0
+google-api-python-client>=1.7.11,<2.43.0
gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0
@@ -13,7 +13,7 @@ rsa>=3.4.2,<4.9.0
# Gmail
google-auth-oauthlib>=0.4.3,<0.6.0
-google-api-python-client>=1.7.11,<2.42.0
+google-api-python-client>=1.7.11,<2.43.0
# goodreads
goodreads>=0.3.2,<0.4.0
diff --git a/requirements.txt b/requirements.txt
index 985bbaf1..7819afe3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
APScheduler>=3.6.3,<3.10.0
+werkzeug<2.1.0
Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1
diff --git a/setup.cfg b/setup.cfg
index 38fed3f6..4497839b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -60,7 +60,7 @@ install_requires =
[options.extras_require]
gdrive =
- google-api-python-client>=1.7.11,<2.37.0
+ google-api-python-client>=1.7.11,<2.43.0
gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0
@@ -73,7 +73,7 @@ gdrive =
rsa>=3.4.2,<4.9.0
gmail =
google-auth-oauthlib>=0.4.3,<0.5.0
- google-api-python-client>=1.7.11,<2.37.0
+ google-api-python-client>=1.7.11,<2.43.0
goodreads =
goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0