Merge branch 'master' into cover_thumbnail

# Conflicts:
#	cps/db.py
#	cps/templates/author.html
#	cps/templates/discover.html
#	cps/templates/index.html
#	cps/templates/search.html
#	cps/templates/shelf.html
#	cps/web.py
#	requirements.txt
#	test/Calibre-Web TestSummary_Linux.html
pull/2262/head^2
Ozzie Isaacs 2 years ago
commit afaf496fbe

@ -45,7 +45,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
4. Calibre-Web can be started afterwards by typing `cps`
In the Wiki there are also examples for a [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation) and for installation on [Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20)
In the Wiki there are also examples for: a [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [installation on Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20), [installation on a Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
## Quick start

@ -16,11 +16,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
try:
from gevent import monkey
monkey.patch_all()
except ImportError:
pass
import sys
import os

@ -1496,7 +1496,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
content.role &= ~constants.ROLE_ANONYMOUS
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
sidebar = get_sidebar_config()
sidebar, __ = get_sidebar_config()
for element in sidebar:
value = element['visibility']
if value in val and not content.check_visibility(value):

@ -680,6 +680,25 @@ class CalibreDB:
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
def generate_linked_query(self, config_read_column, database):
if not config_read_column:
query = (self.session.query(database, ub.ArchivedBook.is_archived, ub.ReadBook.read_status)
.select_from(Books)
.outerjoin(ub.ReadBook,
and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == Books.id)))
else:
try:
read_column = cc_classes[config_read_column]
query = (self.session.query(database, ub.ArchivedBook.is_archived, read_column.value)
.select_from(Books)
.outerjoin(read_column, read_column.book == Books.id))
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} is not existing in calibre database".format(config_read_column))
# Skip linking read column and return None instead of read status
query = self.session.query(database, None, ub.ArchivedBook.is_archived)
return query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id,
int(current_user.id) == ub.ArchivedBook.user_id))
@staticmethod
def get_checkbox_sorted(inputlist, state, offset, limit, order, combo=False):
outcome = list()
@ -709,31 +728,14 @@ class CalibreDB:
join_archive_read, config_read_column, *join):
pagesize = pagesize or self.config.config_books_per_page
if current_user.show_detail_random():
randm = self.session.query(Books) \
.filter(self.common_filters(allow_show_archived)) \
.order_by(func.random()) \
.limit(self.config.config_random_books) \
.all()
random_query = self.generate_linked_query(config_read_column, database)
randm = (random_query.filter(self.common_filters(allow_show_archived))
.order_by(func.random())
.limit(self.config.config_random_books).all())
else:
randm = false()
if join_archive_read:
if not config_read_column:
query = (self.session.query(database, ub.ReadBook.read_status, ub.ArchivedBook.is_archived)
.select_from(Books)
.outerjoin(ub.ReadBook,
and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == Books.id)))
else:
try:
read_column = cc_classes[config_read_column]
query = (self.session.query(database, read_column.value, ub.ArchivedBook.is_archived)
.select_from(Books)
.outerjoin(read_column, read_column.book == Books.id))
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} is not existing in calibre database".format(read_column))
# Skip linking read column and return None instead of read status
query = self.session.query(database, None, ub.ArchivedBook.is_archived)
query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id,
int(current_user.id) == ub.ArchivedBook.user_id))
query = self.generate_linked_query(config_read_column, database)
else:
query = self.session.query(database)
off = int(int(pagesize) * (page - 1))
@ -817,36 +819,21 @@ class CalibreDB:
def check_exists_book(self, authr, title):
self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split(r'\s*&\s*', authr)
for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
author_terms = re.split(r'\s*&\s*', authr)
for author_term in author_terms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + author_term + "%")))
return self.session.query(Books) \
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
def search_query(self, term, config_read_column, *join):
def search_query(self, term, config, *join):
term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split("[, ]+", term)
for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
if not config_read_column:
query = (self.session.query(Books, ub.ArchivedBook.is_archived, ub.ReadBook).select_from(Books)
.outerjoin(ub.ReadBook, and_(Books.id == ub.ReadBook.book_id,
int(current_user.id) == ub.ReadBook.user_id)))
else:
try:
read_column = cc_classes[config_read_column]
query = (self.session.query(Books, ub.ArchivedBook.is_archived, read_column.value).select_from(Books)
.outerjoin(read_column, read_column.book == Books.id))
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} is not existing in calibre database".format(config_read_column))
# Skip linking read column
query = self.session.query(Books, ub.ArchivedBook.is_archived, None)
query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id,
int(current_user.id) == ub.ArchivedBook.user_id))
author_terms = re.split("[, ]+", term)
for author_term in author_terms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + author_term + "%")))
query = self.generate_linked_query(config.config_read_column, Books)
if len(join) == 6:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
if len(join) == 3:
@ -855,20 +842,42 @@ class CalibreDB:
query = query.outerjoin(join[0], join[1])
elif len(join) == 1:
query = query.outerjoin(join[0])
return query.filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%")
))
cc = self.get_cc_columns(config, filter_config_custom_read=True)
filter_expression = [Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%")]
for c in cc:
if c.datatype not in ["datetime", "rating", "bool", "int", "float"]:
filter_expression.append(
getattr(Books,
'custom_column_' + str(c.id)).any(
func.lower(cc_classes[c.id].value).ilike("%" + term + "%")))
return query.filter(self.common_filters(True)).filter(or_(*filter_expression))
def get_cc_columns(self, config, filter_config_custom_read=False):
tmp_cc = self.session.query(CustomColumns).filter(CustomColumns.datatype.notin_(cc_exceptions)).all()
cc = []
r = None
if config.config_columns_to_ignore:
r = re.compile(config.config_columns_to_ignore)
for col in tmp_cc:
if filter_config_custom_read and config.config_read_column and config.config_read_column == col.id:
continue
if r and r.match(col.name):
continue
cc.append(col)
return cc
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None,
config_read_column=False, *join):
def get_search_results(self, term, config, offset=None, order=None, limit=None, *join):
order = order[0] if order else [Books.sort]
pagination = None
result = self.search_query(term, config_read_column, *join).order_by(*order).all()
result = self.search_query(term, config, *join).order_by(*order).all()
result_count = len(result)
if offset != None and limit != None:
offset = int(offset)

@ -32,7 +32,7 @@ try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError
from sqlalchemy.exc import OperationalError, InvalidRequestError, IntegrityError
from sqlalchemy.sql.expression import text
#try:
@ -81,7 +81,7 @@ if gdrive_support:
if not logger.is_debug_enabled():
logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR)
else:
log.debug("Cannot import pydrive, httplib2, using gdrive will not work: %s", importError)
log.debug("Cannot import pydrive, httplib2, using gdrive will not work: {}".format(importError))
class Singleton:
@ -213,7 +213,7 @@ def getDrive(drive=None, gauth=None):
try:
gauth.Refresh()
except RefreshError as e:
log.error("Google Drive error: %s", e)
log.error("Google Drive error: {}".format(e))
except Exception as ex:
log.error_or_exception(ex)
else:
@ -225,7 +225,7 @@ def getDrive(drive=None, gauth=None):
try:
drive.auth.Refresh()
except RefreshError as e:
log.error("Google Drive error: %s", e)
log.error("Google Drive error: {}".format(e))
return drive
def listRootFolders():
@ -234,7 +234,7 @@ def listRootFolders():
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
log.info("GDrive Error %s" % e)
log.info("GDrive Error {}".format(e))
fileList = []
return fileList
@ -272,7 +272,7 @@ def getEbooksFolderId(drive=None):
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: %s', ex)
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return gDriveId.gdrive_id
@ -288,6 +288,7 @@ def getFile(pathId, fileName, drive):
def getFolderId(path, drive):
# drive = getDrive(drive)
currentFolderId = None
try:
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
@ -320,8 +321,8 @@ def getFolderId(path, drive):
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
except OperationalError as ex:
log.error_or_exception('Database error: %s', ex)
except (OperationalError, IntegrityError) as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
except ApiRequestError as ex:
log.error('{} {}'.format(ex.error['message'], path))
@ -545,7 +546,7 @@ def deleteDatabaseOnChange():
session.commit()
except (OperationalError, InvalidRequestError) as ex:
session.rollback()
log.error_or_exception('Database error: %s', ex)
log.error_or_exception('Database error: {}'.format(ex))
def updateGdriveCalibreFromLocal():
@ -563,7 +564,7 @@ def updateDatabaseOnEdit(ID,newPath):
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: %s', ex)
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
@ -573,7 +574,7 @@ def deleteDatabaseEntry(ID):
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: %s', ex)
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
@ -594,7 +595,7 @@ def get_cover_via_gdrive(cover_path):
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: %s', ex)
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return df.metadata.get('webContentLink')
else:
@ -616,7 +617,7 @@ def do_gdrive_download(df, headers, convert_encoding=False):
def stream(convert_encoding):
for byte in s:
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
headers = {"Range": 'bytes={}-{}'.format(byte[0], byte[1])}
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
if resp.status == 206:
if convert_encoding:
@ -624,7 +625,7 @@ def do_gdrive_download(df, headers, convert_encoding=False):
content = content.decode(result['encoding']).encode('utf-8')
yield content
else:
log.warning('An error occurred: %s', resp)
log.warning('An error occurred: {}'.format(resp))
return
return Response(stream_with_context(stream(convert_encoding)), headers=headers)

@ -0,0 +1,29 @@
# -*- 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 <http://www.gnu.org/licenses/>.
from gevent.pywsgi import WSGIHandler
class MyWSGIHandler(WSGIHandler):
def get_environ(self):
env = super().get_environ()
path, __ = self.path.split('?', 1) if '?' in self.path else (self.path, '')
env['RAW_URI'] = path
return env

@ -19,6 +19,7 @@
import os
import io
import sys
import mimetypes
import re
import shutil
@ -226,11 +227,23 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
return _(u"The requested file could not be read. Maybe wrong permissions?")
def shorten_component(s, by_what):
l = len(s)
if l < by_what:
return s
l = (l - by_what)//2
if l <= 0:
return s
return s[:l] + s[-l:]
def get_valid_filename(value, replace_whitespace=True, chars=128):
"""
Returns the given string converted to a string that can be used for a clean
filename. Limits num characters to 128 max.
"""
if value[-1:] == u'.':
value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0')
@ -241,7 +254,10 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
# pipe has to be replaced with comma
value = re.sub(r'[|]+', u',', value, flags=re.U)
value = value[:chars].strip()
filename_encoding_for_length = 'utf-16' if sys.platform == "win32" or sys.platform == "darwin" else 'utf-8'
value = value.encode(filename_encoding_for_length)[:chars].decode('utf-8', errors='ignore').strip()
if not value:
raise ValueError("Filename cannot be empty")
return value
@ -722,7 +738,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
if path:
return redirect(path)
else:
log.error('%s/cover.jpg not found on Google Drive', book.path)
log.error('{}/cover.jpg not found on Google Drive'.format(book.path))
return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as ex:
log.error_or_exception(ex)
@ -1029,24 +1045,6 @@ def check_valid_domain(domain_text):
return not len(result)
def get_cc_columns(filter_config_custom_read=False):
tmpcc = calibre_db.session.query(db.CustomColumns)\
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
cc = []
r = None
if config.config_columns_to_ignore:
r = re.compile(config.config_columns_to_ignore)
for col in tmpcc:
if filter_config_custom_read and config.config_read_column and config.config_read_column == col.id:
continue
if r and r.match(col.name):
continue
cc.append(col)
return cc
def get_download_link(book_id, book_format, client):
book_format = book_format.split(".")[0]
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)

@ -26,7 +26,8 @@ from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_, any_, true
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 .helper import get_download_link, get_book_cover
@ -84,7 +85,7 @@ def feed_osd():
@requires_basic_auth_if_no_ano
def feed_cc_search(query):
# Handle strange query from Libera Reader with + instead of spaces
plus_query = unquote_plus(request.base_url.split('/opds/search/')[1]).strip()
plus_query = unquote_plus(request.environ['RAW_URI'].split('/opds/search/')[1]).strip()
return feed_search(plus_query)
@ -108,7 +109,8 @@ def feed_letter_books(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
letter,
[db.Books.sort])
[db.Books.sort],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -118,15 +120,16 @@ def feed_letter_books(book_id):
def feed_new():
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, True, [db.Books.timestamp.desc()])
db.Books, True, [db.Books.timestamp.desc()],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/discover")
@requires_basic_auth_if_no_ano
def feed_discover():
entries = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).order_by(func.random())\
.limit(config.config_books_per_page)
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -137,7 +140,8 @@ def feed_best_rated():
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
[db.Books.timestamp.desc()])
[db.Books.timestamp.desc()],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -150,11 +154,11 @@ def feed_hot():
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
download_book = calibre_db.get_book(book.Downloads.book_id)
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(
calibre_db.get_filtered_book(book.Downloads.book_id)
)
entries.append(download_book)
else:
ub.delete_download(book.Downloads.book_id)
num_books = entries.__len__()
@ -270,7 +274,8 @@ def feed_series(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index])
[db.Books.series_index],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -324,7 +329,8 @@ def feed_format(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc()])
[db.Books.timestamp.desc()],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -351,7 +357,8 @@ def feed_languages(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
db.Books.languages.any(db.Languages.id == book_id),
[db.Books.timestamp.desc()])
[db.Books.timestamp.desc()],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -381,13 +388,25 @@ def feed_shelf(book_id):
result = list()
# user is allowed to access shelf
if shelf:
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by(
ub.BookShelf.order.asc()).all()
for book in books_in_shelf:
cur_book = calibre_db.get_book(book.book_id)
result.append(cur_book)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(result))
result, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
config.config_books_per_page,
db.Books,
ub.BookShelf.shelf == shelf.id,
[ub.BookShelf.order.asc()],
True, config.config_read_column,
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
# delete shelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf) \
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.filter(db.Books.id == None).all()
for entry in wrong_entries:
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
try:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
ub.session.commit()
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@ -448,11 +467,10 @@ def feed_unread_books():
def feed_search(term):
if term:
entries, __, ___ = calibre_db.get_search_results(term, config_read_column=config.config_read_column)
entries, __, ___ = calibre_db.get_search_results(term, config=config)
entries_count = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entries_count, entries_count)
items = [entry[0] for entry in entries]
return render_xml_template('feed.xml', searchterm=term, entries=items, pagination=pagination)
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
else:
return render_xml_template('feed.xml', searchterm="")
@ -493,14 +511,16 @@ def render_xml_dataset(data_table, book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
getattr(db.Books, data_table.__tablename__).any(data_table.id == book_id),
[db.Books.timestamp.desc()])
[db.Books.timestamp.desc()],
True, config.config_read_column)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
def render_element_index(database_column, linked_table, folder):
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(database_column, 1, 1)).label('id'))
entries = calibre_db.session.query(func.upper(func.substr(database_column, 1, 1)).label('id'), None, None)
# query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
if linked_table is not None:
entries = entries.join(linked_table).join(db.Books)
entries = entries.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(database_column, 1, 1))).all()

@ -16,7 +16,7 @@
# 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 flask import render_template
from flask import render_template, request
from flask_babel import gettext as _
from flask import g
from werkzeug.local import LocalProxy
@ -30,6 +30,8 @@ log = logger.create()
def get_sidebar_config(kwargs=None):
kwargs = kwargs or []
simple = bool([e for e in ['kindle', 'tolino', "kobo", "bookeen"]
if (e in request.headers.get('User-Agent', "").lower())])
if 'content' in kwargs:
content = kwargs['content']
content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous()
@ -93,14 +95,14 @@ def get_sidebar_config(kwargs=None):
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
return sidebar
if not simple:
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
return sidebar, simple
def get_readbooks_ids():
'''def get_readbooks_ids():
if not config.config_read_column:
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\
.filter(ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED).all()
@ -112,11 +114,11 @@ def get_readbooks_ids():
return frozenset([x.book for x in readBooks])
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
return []
return []'''
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD, read_book_ids=get_readbooks_ids(),
sidebar, simple = get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple,
accept=constants.EXTENSIONS_UPLOAD, # read_book_ids=get_readbooks_ids(),
*args, **kwargs)

@ -23,7 +23,7 @@ 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
@ -32,7 +32,7 @@ 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
from . import constants, get_locale, logger, ub, web_server
# current_milli_time = lambda: int(round(time() * 1000))
@ -40,6 +40,14 @@ meta = Blueprint("metadata", __name__)
log = logger.create()
try:
from dataclasses import asdict
except ImportError:
log.info('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***')
print('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***')
web_server.stop(True)
sys.exit(6)
new_list = list()
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))

@ -25,6 +25,7 @@ import subprocess # nosec
try:
from gevent.pywsgi import WSGIServer
from .gevent_wsgi import MyWSGIHandler
from gevent.pool import Pool
from gevent import __version__ as _version
from greenlet import GreenletExit
@ -32,7 +33,7 @@ try:
VERSION = 'Gevent ' + _version
_GEVENT = True
except ImportError:
from tornado.wsgi import WSGIContainer
from .tornado_wsgi import MyWSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado import version as _version
@ -202,7 +203,8 @@ class WebServer(object):
if output is None:
output = _readable_listen_address(self.listen_address, self.listen_port)
log.info('Starting Gevent server on %s', output)
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, spawn=Pool(), **ssl_args)
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
spawn=Pool(), **ssl_args)
if ssl_args:
wrap_socket = self.wsgiserver.wrap_socket
def my_wrap_socket(*args, **kwargs):
@ -225,8 +227,8 @@ class WebServer(object):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port))
# Max Buffersize set to 200MB )
http_server = HTTPServer(WSGIContainer(self.app),
# Max Buffersize set to 200MB
http_server = HTTPServer(MyWSGIContainer(self.app),
max_buffer_size=209700000,
ssl_options=self.ssl_args)
http_server.listen(self.listen_port, self.listen_address)

@ -439,7 +439,7 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
False, 0,
True, config.config_read_column,
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf) \

@ -47,7 +47,9 @@
{% endfor %}
</table>
{% endif %}
<a class="btn btn-default" id="admin_user_table" href="{{url_for('admin.edit_user_table')}}">{{_('Edit Users')}}</a>
{% if not simple %}
<a class="btn btn-default" id="admin_user_table" href="{{url_for('admin.edit_user_table')}}">{{_('Edit Users')}}</a>
{% endif %}
<a class="btn btn-default" id="admin_new_user" href="{{url_for('admin.new_user')}}">{{_('Add New User')}}</a>
{% if (config.config_login_type == 1) %}
<div class="btn btn-default" id="import_ldap_users" data-toggle="modal" data-target="#StatusDialog">{{_('Import LDAP Users')}}</div>

@ -31,23 +31,22 @@
<a id="pub_old" data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" class="btn btn-primary{% if order == "pubold" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
</div>
<div class="row display-flex">
{% if entries[0] %}
{% for entry in entries %}
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img" title="{{entry.title}}">
{{ image.book_cover(entry, alt=author.name|safe) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{entry.Books.title}}">
{{ image.book_cover(entry.Books, alt=author.name|safe) }}
{% if entry[2] == True %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
<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 %}>
<p title="{{ entry.Books.title }}" class="title">{{entry.Books.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.Books.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<span class="author-hidden-divider">&amp;</span>
@ -63,23 +62,23 @@
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %}
{% endfor %}
{% for format in entry.data %}
{% for format in entry.Books.data %}
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
<span class="glyphicon glyphicon-music"></span>
{% endif %}
{% endfor %}
</p>
{% if entry.series.__len__() > 0 %}
{% if entry.Books.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.Books.series[0].id )}}">
{{entry.Books.series[0].name}}
</a>
({{entry.series_index|formatseriesindex}})
({{entry.Books.series_index|formatseriesindex}})
</p>
{% endif %}
{% if entry.ratings.__len__() > 0 %}
{% if entry.Books.ratings.__len__() > 0 %}
<div class="rating">
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
{% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
@ -92,7 +91,6 @@
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
@ -110,7 +108,7 @@
<div class="meta">
<p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
<a class="author-name author-hidden" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
{% if loop.last %}

@ -1,3 +1,3 @@
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="title">{{entry.title|shortentitle}}</span>
</a>
</a>

@ -162,8 +162,10 @@
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if conf.show_detail_random() %}checked{% endif %}>
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div>
{% if not simple %}
<a href="#" id="get_tags" data-id="0" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_column_values" data-id="0" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied custom column values')}}</a>
{% endif %}
</div>
</div>
</div>

@ -1,66 +0,0 @@
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
<div class="discover load-more">
<h2>{{title}}</h2>
<div class="row display-flex">
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
{% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img" title="{{entry.title}}">
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
{% endif %}
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.ordered_authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %}
{% endfor %}
</p>
{% if entry.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
</a>
({{entry.series_index|formatseriesindex}})
</p>
{% endif %}
{% if entry.ratings.__len__() > 0 %}
<div class="rating">
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
<span class="glyphicon glyphicon-star-empty"></span>
{% endfor %}
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -69,7 +69,7 @@
{% endif %}
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Back')}}</a>
</form>
{% if g.allow_registration %}
{% if g.allow_registration and not simple%}
<div class="col-md-10 col-lg-6">
<h2>{{_('Allowed Domains (Whitelist)')}}</h2>
<form id="domain_add_allow" action="{{ url_for('admin.add_domain',allow=1)}}" method="POST">

@ -40,35 +40,35 @@
{% if entries and entries[0] %}
{% for entry in entries %}
<entry>
<title>{{entry.title}}</title>
<id>urn:uuid:{{entry.uuid}}</id>
<updated>{{entry.atom_timestamp}}</updated>
{% if entry.authors.__len__() > 0 %}
<title>{{entry.Books.title}}</title>
<id>urn:uuid:{{entry.Books.uuid}}</id>
<updated>{{entry.Books.atom_timestamp}}</updated>
{% if entry.Books.authors.__len__() > 0 %}
<author>
<name>{{entry.authors[0].name}}</name>
<name>{{entry.Books.authors[0].name}}</name>
</author>
{% endif %}
{% if entry.publishers.__len__() > 0 %}
{% if entry.Books.publishers.__len__() > 0 %}
<publisher>
<name>{{entry.publishers[0].name}}</name>
<name>{{entry.Books.publishers[0].name}}</name>
</publisher>
{% endif %}
{% for lang in entry.languages %}
{% for lang in entry.Books.languages %}
<dcterms:language>{{lang.lang_code}}</dcterms:language>
{% endfor %}
{% for tag in entry.tags %}
{% for tag in entry.Books.tags %}
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html"
term="{{tag.name}}"
label="{{tag.name}}"/>
{% endfor %}
{% if entry.comments[0] %}<summary>{{entry.comments[0].text|striptags}}</summary>{% endif %}
{% if entry.has_cover %}
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image"/>
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image/thumbnail"/>
{% if entry.Books.comments[0] %}<summary>{{entry.Books.comments[0].text|striptags}}</summary>{% endif %}
{% if entry.Books.has_cover %}
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image"/>
<link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.Books.id)}}" rel="http://opds-spec.org/image/thumbnail"/>
{% endif %}
{% for format in entry.data %}
<link rel="http://opds-spec.org/acquisition" href="{{ url_for('opds.opds_download_link', book_id=entry.id, book_format=format.format|lower)}}"
length="{{format.uncompressed_size}}" mtime="{{entry.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/>
{% for format in entry.Books.data %}
<link rel="http://opds-spec.org/acquisition" href="{{ url_for('opds.opds_download_link', book_id=entry.Books.id, book_format=format.format|lower)}}"
length="{{format.uncompressed_size}}" mtime="{{entry.Books.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/>
{% endfor %}
</entry>
{% endfor %}

@ -1,31 +1,31 @@
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
{% if g.user.show_detail_random() %}
{% if g.user.show_detail_random() and page != "discover" %}
<div class="discover random-books">
<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="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img" title="{{ entry.title }}">
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{ entry.Books.title }}">
{{ image.book_cover(entry.Books) }}
{% if entry[2] == True %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
<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 %}>
<p title="{{entry.Books.title}}" class="title">{{entry.Books.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.Books.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
@ -37,17 +37,17 @@
{% endif %}
{% endfor %}
</p>
{% if entry.series.__len__() > 0 %}
{% if entry.Books.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.Books.series[0].id )}}">
{{entry.Books.series[0].name}}
</a>
({{entry.series_index|formatseriesindex}})
({{entry.Books.series_index|formatseriesindex}})
</p>
{% endif %}
{% if entry.ratings.__len__() > 0 %}
{% if entry.Books.ratings.__len__() > 0 %}
<div class="rating">
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
{% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
@ -65,6 +65,7 @@
{% endif %}
<div class="discover load-more">
<h2 class="{{title}}">{{title}}</h2>
{% if page != 'discover' %}
<div class="filterheader hidden-xs">
{% if page == 'hot' %}
<a data-toggle="tooltip" title="{{_('Sort ascending according to download count')}}" id="hot_asc" class="btn btn-primary{% if order == "hotasc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='hotasc')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
@ -84,25 +85,25 @@
{% endif %}
{% endif %}
</div>
{% endif %}
<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="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img" title="{{ entry.title }}">
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{ entry.Books.title }}">
{{ image.book_cover(entry.Books) }}
{% if entry[2] == True %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
<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 %}>
<p title="{{ entry.Books.title }}" class="title">{{entry.Books.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.Books.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<span class="author-hidden-divider">&amp;</span>
@ -118,23 +119,27 @@
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='stored') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %}
{% endfor %}
{% for format in entry.data %}
{% for format in entry.Books.data %}
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
<span class="glyphicon glyphicon-music"></span>
{% endif %}
{%endfor%}
</p>
{% if entry.series.__len__() > 0 %}
{% if entry.Books.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
{% if page != "series" %}
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.Books.series[0].id )}}">
{{entry.Books.series[0].name}}
</a>
({{entry.series_index|formatseriesindex}})
{% else %}
<span>{{entry.Books.series[0].name}}</span>
{% endif %}
({{entry.Books.series_index|formatseriesindex}})
</p>
{% endif %}
{% if entry.ratings.__len__() > 0 %}
{% if entry.Books.ratings.__len__() > 0 %}
<div class="rating">
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
{% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}

@ -70,7 +70,7 @@
</form>
</li>
{% endif %}
{% if not g.user.is_anonymous %}
{% if not g.user.is_anonymous and not simple%}
<li><a id="top_tasks" href="{{url_for('web.get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span> <span class="hidden-sm">{{_('Tasks')}}</span></a></li>
{% endif %}
{% if g.user.role_admin() %}

@ -44,16 +44,16 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
{% if entry.Books.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<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}}" >
{{ image.book_cover(entry.Books) }}
{% if entry.Books.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
{% if entry[2] == True %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
{% endif %}
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<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 %}>
<p title="{{entry.Books.title}}" class="title">{{entry.Books.title|shortentitle}}</p>
</a>
<p class="author">

@ -33,19 +33,19 @@
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img" title="{{entry.title}}" >
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{entry.Books.title}}" >
{{ image.book_cover(entry.Books) }}
{% if entry[2] == True %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
<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 %}>
<p title="{{entry.Books.title}}" class="title">{{entry.Books.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.Books.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<span class="author-hidden-divider">&amp;</span>
@ -62,17 +62,17 @@
{% endif %}
{% endfor %}
</p>
{% if entry.series.__len__() > 0 %}
{% if entry.Books.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.Books.series[0].id )}}">
{{entry.Books.series[0].name}}
</a>
({{entry.series_index|formatseriesindex}})
({{entry.Books.series_index|formatseriesindex}})
</p>
{% endif %}
{% if entry.ratings.__len__() > 0 %}
{% if entry.Books.ratings.__len__() > 0 %}
<div class="rating">
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
{% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}

@ -35,31 +35,31 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="meta">
<p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
<p title="{{entry.Books.title}}" class="title">{{entry.Books.title|shortentitle}}</p>
<p class="author">
{% for author in entry.ordered_authors %}
{% for author in entry.Books.authors %}
<a href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %}
&amp;
{% endif %}
{% endfor %}
</p>
{% if entry.series.__len__() > 0 %}
{% if entry.Books.series.__len__() > 0 %}
<p class="series">
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id )}}">
{{entry.series[0].name}}
<a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.Books.series[0].id )}}">
{{entry.Books.series[0].name}}
</a>
({{entry.series_index}})
({{entry.Books.series_index}})
</p>
{% endif %}
</div>
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
{% if g.user.role_download() %}
{% if entry.data|length %}
{% if entry.Books.data|length %}
<div class="btn-group" role="group">
{% for format in entry.data %}
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
{% for format in entry.Books.data %}
<a href="{{ url_for('web.download_link', book_id=entry.Books.id, book_format=format.format|lower, anyname=entry.Books.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.Books.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
</a>
{% endfor %}

@ -83,7 +83,7 @@
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div>
{% if ( g.user and g.user.role_admin() and not new_user ) %}
{% if ( g.user and g.user.role_admin() and not new_user ) and not simple %}
<a href="#" id="get_user_tags" class="btn btn-default" data-id="{{content.id}}" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_user_column_values" data-id="{{content.id}}" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
{% endif %}
@ -131,7 +131,7 @@
</div>
{% endif %}
{% endif %}
{% if kobo_support and not content.role_anonymous() %}
{% if kobo_support and not content.role_anonymous() and not simple%}
<div class="form-group">
<input type="checkbox" name="kobo_only_shelves_sync" id="kobo_only_shelves_sync" {% if content.kobo_only_shelves_sync %}checked{% endif %}>
<label for="kobo_only_shelves_sync">{{_('Sync only books in selected shelves with Kobo')}}</label>

@ -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 <http://www.gnu.org/licenses/>.
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

@ -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)

@ -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

@ -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

@ -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

Loading…
Cancel
Save