1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-30 03:50:31 +00:00
calibre-web/cps/web.py
Aleksei Besogonov 01215aaa68
Allow the web reader to use KEPUB files and print file sizes
This commit allows the web reader to read the KEPUB (Kobo EPUB) files,
and also shows the file sizes. This is especially useful if you have
giant EPUB files with built-in audiobooks and media overlays.
2024-12-08 23:30:05 -08:00

1670 lines
82 KiB
Python

# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# 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/>.
import os
import json
import mimetypes
import chardet # dependency of requests
import copy
from importlib.metadata import metadata
from flask import Blueprint, jsonify, request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session
from flask_babel import gettext as _
from flask_babel import get_locale
from .cw_login import login_user, logout_user, current_user
from flask_limiter import RateLimitExceeded
from flask_limiter.util import get_remote_address
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql.functions import coalesce
from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash
from . import constants, logger, isoLanguages, services
from . import db, ub, config, app
from . import calibre_db, kobo_sync_status
from .search import render_search_results, render_adv_search_results
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, 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_ereader, check_read_formats, tags_filters, reset_password, valid_email, \
edit_book_read_status, valid_password
from .pagination import Pagination
from .redirect import get_redirect_location
from .cw_babel import get_available_locale
from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template
from .kobo_sync_status import change_archived_books
from . import limiter
from .services.worker import WorkerThread
from .tasks_status import render_task_status
from .usermanagement import user_login_required
from .string_helper import strip_whitespaces
feature_support = {
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo)
}
try:
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
feature_support['oauth'] = True
except ImportError:
feature_support['oauth'] = False
oauth_check = {}
register_user_with_oauth = logout_oauth_user = get_oauth_status = None
from functools import wraps
try:
from natsort import natsorted as sort
except ImportError:
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
sql_version = metadata("sqlalchemy")["Version"]
sqlalchemy_version2 = ([int(x) if x.isnumeric() else 0 for x in sql_version.split('.')[:3]] >= [2, 0, 0])
@app.after_request
def add_security_headers(resp):
default_src = ([host.strip() for host in config.config_trustedhosts.split(',') if host] +
["'self'", "'unsafe-inline'", "'unsafe-eval'"])
csp = "default-src " + ' '.join(default_src)
if request.endpoint == "web.read_book" and config.config_use_google_drive:
csp +=" blob: "
csp += "; font-src 'self' data:"
if request.endpoint == "web.read_book":
csp += " blob: "
csp += "; 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:"
if request.endpoint == "edit-book.show_edit_book" or config.config_use_google_drive:
csp += " *"
if request.endpoint == "web.read_book":
csp += " blob: ; style-src-elem 'self' blob: 'unsafe-inline'"
csp += "; object-src 'none';"
resp.headers['Content-Security-Policy'] = csp
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
resp.headers['X-XSS-Protection'] = '1; mode=block'
resp.headers['Strict-Transport-Security'] = 'max-age=31536000';
return resp
web = Blueprint('web', __name__)
log = logger.create()
# ################################### Login logic and rights management ###############################################
def download_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_download():
return f(*args, **kwargs)
abort(403)
return inner
def viewer_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_viewer():
return f(*args, **kwargs)
abort(403)
return inner
# ################################### data provider functions #########################################################
@web.route("/ajax/emailstat")
@user_login_required
def get_email_status_json():
tasks = WorkerThread.get_instance().tasks
return jsonify(render_task_status(tasks))
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
@user_login_required
def set_bookmark(book_id, book_format):
bookmark_key = request.form["bookmark"]
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
ub.Bookmark.book_id == book_id,
ub.Bookmark.format == book_format)).delete()
if not bookmark_key:
ub.session_commit()
return "", 204
l_bookmark = ub.Bookmark(user_id=current_user.id,
book_id=book_id,
format=book_format,
bookmark_key=bookmark_key)
ub.session.merge(l_bookmark)
ub.session_commit("Bookmark for user {} in book {} created".format(current_user.id, book_id))
return "", 201
@web.route("/ajax/toggleread/<int:book_id>", methods=['POST'])
@user_login_required
def toggle_read(book_id):
message = edit_book_read_status(book_id)
if message:
return message, 400
else:
return message
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
@user_login_required
def toggle_archived(book_id):
change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
# Remove book from syncd books list to force resync (?)
remove_synced_book(book_id)
return ""
@web.route("/ajax/view", methods=["POST"])
@login_required_if_no_ano
def update_view():
to_save = request.get_json()
try:
for element in to_save:
for param in to_save[element]:
current_user.set_view_property(element, param, to_save[element][param])
except Exception as ex:
log.error("Could not save view_settings: %r %r: %e", request, to_save, ex)
return "Invalid request", 400
return "1", 200
'''
@web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>")
@user_login_required
def get_comic_book(book_id, book_format, page):
book = calibre_db.get_book(book_id)
if not book:
return "", 204
else:
for bookformat in book.data:
if bookformat.format.lower() == book_format.lower():
cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format
if book_format in ("cbr", "rar"):
if feature_support['rar'] == True:
rarfile.UNRAR_TOOL = config.config_rarfile_location
try:
rf = rarfile.RarFile(cbr_file)
names = sort(rf.namelist())
extract = lambda page: rf.read(names[page])
except:
# rarfile not valid
log.error('Unrar binary not found, or unable to decompress file %s', cbr_file)
return "", 204
else:
log.info('Unrar is not supported please install python rarfile extension')
# no support means return nothing
return "", 204
elif book_format in ("cbz", "zip"):
zf = zipfile.ZipFile(cbr_file)
names=sort(zf.namelist())
extract = lambda page: zf.read(names[page])
elif book_format in ("cbt", "tar"):
tf = tarfile.TarFile(cbr_file)
names=sort(tf.getnames())
extract = lambda page: tf.extractfile(names[page]).read()
else:
log.error('unsupported comic format')
return "", 204
b64 = codecs.encode(extract(page), 'base64').decode()
ext = names[page].rpartition('.')[-1]
if ext not in ('png', 'gif', 'jpg', 'jpeg', 'webp'):
ext = 'png'
extractedfile="data:image/" + ext + ";base64," + b64
fileData={"name": names[page], "page":page, "last":len(names)-1, "content": extractedfile}
return make_response(json.dumps(fileData))
return "", 204
'''
# ################################### Typeahead ##################################################################
@web.route("/get_authors_json", methods=['GET'])
@login_required_if_no_ano
def get_authors_json():
return calibre_db.get_typeahead(db.Authors, request.args.get('q'), ('|', ','))
@web.route("/get_publishers_json", methods=['GET'])
@login_required_if_no_ano
def get_publishers_json():
return calibre_db.get_typeahead(db.Publishers, request.args.get('q'), ('|', ','))
@web.route("/get_tags_json", methods=['GET'])
@login_required_if_no_ano
def get_tags_json():
return calibre_db.get_typeahead(db.Tags, request.args.get('q'), tag_filter=tags_filters())
@web.route("/get_series_json", methods=['GET'])
@login_required_if_no_ano
def get_series_json():
return calibre_db.get_typeahead(db.Series, request.args.get('q'))
@web.route("/get_languages_json", methods=['GET'])
@login_required_if_no_ano
def get_languages_json():
query = (request.args.get('q') or '').lower()
language_names = isoLanguages.get_language_names(get_locale())
entries_start = [s for key, s in language_names.items() if s.lower().startswith(query.lower())]
if len(entries_start) < 5:
entries = [s for key, s in language_names.items() if query in s.lower()]
entries_start.extend(entries[0:(5 - len(entries_start))])
entries_start = list(set(entries_start))
json_dumps = json.dumps([dict(name=r) for r in entries_start[0:5]])
return json_dumps
@web.route("/get_matching_tags", methods=['GET'])
@login_required_if_no_ano
def get_matching_tags():
tag_dict = {'tags': []}
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True))
calibre_db.create_functions()
# calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
author_input = request.args.get('authors') or ''
title_input = request.args.get('title') or ''
include_tag_inputs = request.args.getlist('include_tag') or ''
exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
func.lower(db.Books.title).ilike("%" + title_input + "%"))
if len(include_tag_inputs) > 0:
for tag in include_tag_inputs:
q = q.filter(db.Books.tags.any(db.Tags.id == tag))
if len(exclude_tag_inputs) > 0:
for tag in exclude_tag_inputs:
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
for book in q:
for tag in book.tags:
if tag.id not in tag_dict['tags']:
tag_dict['tags'].append(tag.id)
json_dumps = json.dumps(tag_dict)
return json_dumps
def generate_char_list(entries): # data_colum, db_link):
char_list = list()
for entry in entries:
upper_char = entry[0].name[0].upper()
if upper_char not in char_list:
char_list.append(upper_char)
return char_list
def query_char_list(data_colum, db_link):
results = (calibre_db.session.query(func.upper(func.substr(data_colum, 1, 1)).label('char'))
.join(db_link).join(db.Books).filter(calibre_db.common_filters())
.group_by(func.upper(func.substr(data_colum, 1, 1))).all())
return results
def get_sort_function(sort_param, data):
order = [db.Books.timestamp.desc()]
if sort_param == 'stored':
sort_param = current_user.get_view_property(data, 'stored')
else:
current_user.set_view_property(data, 'stored', sort_param)
if sort_param == 'pubnew':
order = [db.Books.pubdate.desc()]
if sort_param == 'pubold':
order = [db.Books.pubdate]
if sort_param == 'abc':
order = [db.Books.sort]
if sort_param == 'zyx':
order = [db.Books.sort.desc()]
if sort_param == 'new':
order = [db.Books.timestamp.desc()]
if sort_param == 'old':
order = [db.Books.timestamp]
if sort_param == 'authaz':
order = [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index]
if sort_param == 'authza':
order = [db.Books.author_sort.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
if sort_param == 'seriesasc':
order = [db.Books.series_index.asc()]
if sort_param == 'seriesdesc':
order = [db.Books.series_index.desc()]
if sort_param == 'hotdesc':
order = [func.count(ub.Downloads.book_id).desc()]
if sort_param == 'hotasc':
order = [func.count(ub.Downloads.book_id).asc()]
if sort_param is None:
sort_param = "new"
return order, sort_param
def render_books_list(data, sort_param, book_id, page):
order = get_sort_function(sort_param, data)
if data == "rated":
return render_rated_books(page, book_id, order=order)
elif data == "discover":
return render_discover_books(book_id)
elif data == "unread":
return render_read_books(page, False, order=order)
elif data == "read":
return render_read_books(page, True, order=order)
elif data == "hot":
return render_hot_books(page, order)
elif data == "download":
return render_downloaded_books(page, order, book_id)
elif data == "author":
return render_author_books(page, book_id, order)
elif data == "publisher":
return render_publisher_books(page, book_id, order)
elif data == "series":
return render_series_books(page, book_id, order)
elif data == "ratings":
return render_ratings_books(page, book_id, order)
elif data == "formats":
return render_formats_books(page, book_id, order)
elif data == "category":
return render_category_books(page, book_id, order)
elif data == "language":
return render_language_books(page, book_id, order)
elif data == "archived":
return render_archived_books(page, order)
elif data == "search":
term = request.args.get('query', None)
offset = int(int(config.config_books_per_page) * (page - 1))
return render_search_results(term, offset, order, config.config_books_per_page)
elif data == "advsearch":
term = json.loads(flask_session.get('query', '{}'))
offset = int(int(config.config_books_per_page) * (page - 1))
return render_adv_search_results(term, offset, order, config.config_books_per_page)
else:
website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0],
True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_("Books"), page=website, order=order[1])
def render_rated_books(page, book_id, order):
if current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.ratings.any(db.Ratings.rating > 9),
order[0],
True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
id=book_id, title=_("Top Rated Books"), page="rated", order=order[1])
else:
abort(404)
def render_discover_books(book_id):
if current_user.check_visibility(constants.SIDEBAR_RANDOM):
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('index.html', random=false(), entries=entries, pagination=pagination, id=book_id,
title=_("Discover (Random Books)"), page="discover")
else:
abort(404)
def render_hot_books(page, order):
if current_user.check_visibility(constants.SIDEBAR_HOT):
if order[1] not in ['hotasc', 'hotdesc']:
# Unary expression comparison only working (for this expression) in sqlalchemy 1.4+
# if not (order[0][0].compare(func.count(ub.Downloads.book_id).desc()) or
# 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_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:
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:
ub.delete_download(book.Downloads.book_id)
num_books = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, num_books)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_("Hot Books (Most Downloaded)"), page="hot", order=order[1])
else:
abort(404)
def render_downloaded_books(page, order, user_id):
if current_user.role_admin():
user_id = int(user_id)
else:
user_id = current_user.id
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and user:
entries, random, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == user_id,
order[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.Books.id).first()):
ub.delete_download(book.Books.id)
return render_title_template('index.html',
random=random,
entries=entries,
pagination=pagination,
id=user_id,
title=_("Downloaded books by %(user)s", user=user.name),
page="download",
order=order[1])
else:
abort(404)
def render_author_books(page, author_id, order):
entries, __, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.authors.any(db.Authors.id == author_id),
[order[0][0], db.Series.name, db.Books.series_index],
True, config.config_read_column,
db.books_series_link,
db.books_series_link.c.book == db.Books.id,
db.Series)
if entries is None or not len(entries):
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
if sqlalchemy_version2:
author = calibre_db.session.get(db.Authors, author_id)
else:
author = calibre_db.session.query(db.Authors).get(author_id)
author_name = author.name.replace('|', ',')
author_info = None
other_books = []
if services.goodreads_support and config.config_use_goodreads:
author_info = services.goodreads_support.get_author_info(author_name)
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=_("Author: %(name)s", name=author_name), author=author_info,
other_books=other_books, page="author", order=order[1])
def render_publisher_books(page, book_id, order):
if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Publishers.name == None,
[db.Series.name, order[0][0], db.Books.series_index],
True, config.config_read_column,
db.books_publishers_link,
db.Books.id == db.books_publishers_link.c.book,
db.Publishers,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
publisher = _("None")
else:
publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
if publisher:
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.publishers.any(
db.Publishers.id == book_id),
[db.Series.name, order[0][0],
db.Books.series_index],
True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
publisher = publisher.name
else:
abort(404)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_("Publisher: %(name)s", name=publisher),
page="publisher",
order=order[1])
def render_series_books(page, book_id, order):
if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Series.name == None,
[order[0][0]],
True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
series_name = _("None")
else:
series_name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
if series_name:
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.series.any(db.Series.id == book_id),
[order[0][0]],
True, config.config_read_column)
series_name = series_name.name
else:
abort(404)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_("Series: %(serie)s", serie=series_name), page="series", order=order[1])
def render_ratings_books(page, book_id, order):
if book_id == '-1':
db_filter = coalesce(db.Ratings.rating, 0) < 1
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db_filter,
[order[0][0]],
True, config.config_read_column,
db.books_ratings_link,
db.Books.id == db.books_ratings_link.c.book,
db.Ratings)
title = _("Rating: None")
else:
name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
if name:
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.ratings.any(db.Ratings.id == book_id),
[order[0][0]],
True, config.config_read_column)
title = _("Rating: %(rating)s stars", rating=int(name.rating / 2))
else:
abort(404)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=title, page="ratings", order=order[1])
def render_formats_books(page, book_id, order):
if book_id == '-1':
name = _("None")
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Data.format == None,
[order[0][0]],
True, config.config_read_column,
db.Data)
else:
name = calibre_db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first()
if name:
name = name.format
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.data.any(
db.Data.format == book_id.upper()),
[order[0][0]],
True, config.config_read_column)
else:
abort(404)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_("File format: %(format)s", format=name),
page="formats",
order=order[1])
def render_category_books(page, book_id, order):
if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Tags.name == None,
[order[0][0], db.Series.name, db.Books.series_index],
True, config.config_read_column,
db.books_tags_link,
db.Books.id == db.books_tags_link.c.book,
db.Tags,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
tagsname = _("None")
else:
tagsname = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
if tagsname:
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.tags.any(db.Tags.id == book_id),
[order[0][0], db.Series.name,
db.Books.series_index],
True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
tagsname = tagsname.name
else:
abort(404)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_("Category: %(name)s", name=tagsname), page="category", order=order[1])
def render_language_books(page, name, order):
try:
if name.lower() != "none":
lang_name = isoLanguages.get_language_name(get_locale(), name)
if lang_name == "Unknown":
abort(404)
else:
lang_name = _("None")
except KeyError:
abort(404)
if name == "none":
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Languages.lang_code == None,
[order[0][0]],
True, config.config_read_column,
db.books_languages_link,
db.Books.id == db.books_languages_link.c.book,
db.Languages)
else:
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.languages.any(db.Languages.lang_code == name),
[order[0][0]],
True, config.config_read_column)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_("Language: %(name)s", name=lang_name), page="language", order=order[1])
def render_read_books(page, are_read, as_xml=False, order=None):
sort_param = order[0] if order else []
if not config.config_read_column:
if are_read:
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
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
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
if not as_xml:
flash(_("Custom Column No.%(column)d does not exist in calibre database",
column=config.config_read_column),
category="error")
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:
if are_read:
name = _('Read Books') + ' (' + str(pagination.total_count) + ')'
page_name = "read"
else:
name = _('Unread Books') + ' (' + str(pagination.total_count) + ')'
page_name = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=page_name, order=order[1])
def render_archived_books(page, sort_param):
order = sort_param[0] or []
archived_books = (ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.is_archived == True)
.all())
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.in_(archived_book_ids)
entries, random, pagination = calibre_db.fill_indexpage_with_archived_books(page, db.Books,
0,
archived_filter,
order,
True,
True, config.config_read_column)
name = _('Archived Books') + ' (' + str(len(entries)) + ')'
page_name = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=page_name, order=sort_param[1])
# ################################### View Books list ##################################################################
@web.route("/", defaults={'page': 1})
@web.route('/page/<int:page>')
@login_required_if_no_ano
def index(page):
sort_param = (request.args.get('sort') or 'stored').lower()
return render_books_list("newest", sort_param, 1, page)
@web.route('/<data>/<sort_param>', defaults={'page': 1, 'book_id': 1})
@web.route('/<data>/<sort_param>/', defaults={'page': 1, 'book_id': 1})
@web.route('/<data>/<sort_param>/<book_id>', defaults={'page': 1})
@web.route('/<data>/<sort_param>/<book_id>/<int:page>')
@login_required_if_no_ano
def books_list(data, sort_param, book_id, page):
return render_books_list(data, sort_param, book_id, page)
@web.route("/table")
@user_login_required
def books_table():
visibility = current_user.view_settings.get('table', {})
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_title_template('book_table.html', title=_("Books List"), cc=cc, page="book_table",
visiblility=visibility)
@web.route("/ajax/listbooks")
@user_login_required
def list_books():
off = int(request.args.get("offset") or 0)
limit = int(request.args.get("limit") or config.config_books_per_page)
search_param = request.args.get("search")
sort_param = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
join = tuple()
if sort_param == "state":
state = json.loads(request.args.get("state", "[]"))
elif sort_param == "tags":
order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()]
join = db.books_tags_link, db.Books.id == db.books_tags_link.c.book, db.Tags
elif sort_param == "series":
order = [db.Series.name.asc()] if order == "asc" else [db.Series.name.desc()]
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
elif sort_param == "publishers":
order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()]
join = db.books_publishers_link, db.Books.id == db.books_publishers_link.c.book, db.Publishers
elif sort_param == "authors":
order = [db.Authors.name.asc(), db.Series.name, db.Books.series_index] if order == "asc" \
else [db.Authors.name.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
join = db.books_authors_link, db.Books.id == db.books_authors_link.c.book, db.Authors, db.books_series_link, \
db.Books.id == db.books_series_link.c.book, db.Series
elif sort_param == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()]
join = db.books_languages_link, db.Books.id == db.books_languages_link.c.book, db.Languages
elif order and sort_param in ["sort", "title", "authors_sort", "series_index"]:
order = [text(sort_param + " " + order)]
elif not state:
order = [db.Books.timestamp.desc()]
total_count = filtered_count = calibre_db.session.query(db.Books).filter(
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).all()
filtered_count = len(books)
else:
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,
*join)
else:
entries, __, __ = calibre_db.fill_indexpage_with_archived_books((int(off) / (int(limit)) + 1),
db.Books,
limit,
True,
order,
True,
True,
config.config_read_column,
*join)
result = list()
for entry in entries:
val = entry[0]
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)
result.append(val)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": result}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@web.route("/ajax/table_settings", methods=['POST'])
@user_login_required
def update_table_settings():
current_user.view_settings['table'] = json.loads(request.data)
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except (InvalidRequestError, OperationalError):
log.error("Invalid request received: %r ", request, )
return "Invalid request", 400
return ""
@web.route("/author")
@login_required_if_no_ano
def author_list():
if current_user.check_visibility(constants.SIDEBAR_AUTHOR):
if current_user.get_view_property('author', 'dir') == 'desc':
order = db.Authors.sort.desc()
order_no = 0
else:
order = db.Authors.sort.asc()
order_no = 1
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(order).all()
char_list = query_char_list(db.Authors.sort, db.books_authors_link)
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
# starts a change session
author_copy = copy.deepcopy(entries)
for entry in author_copy:
entry.Authors.name = entry.Authors.name.replace('|', ',')
return render_title_template('list.html', entries=author_copy, folder='web.books_list', charlist=char_list,
title="Authors", page="authorlist", data='author', order=order_no)
else:
abort(404)
@web.route("/downloadlist")
@login_required_if_no_ano
def download_list():
if current_user.get_view_property('download', 'dir') == 'desc':
order = ub.User.name.desc()
order_no = 0
else:
order = ub.User.name.asc()
order_no = 1
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and current_user.role_admin():
entries = ub.session.query(ub.User, func.count(ub.Downloads.book_id).label('count')) \
.join(ub.Downloads).group_by(ub.Downloads.user_id).order_by(order).all()
char_list = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) \
.group_by(func.upper(func.substr(ub.User.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_("Downloads"), page="downloadlist", data="download", order=order_no)
else:
abort(404)
@web.route("/publisher")
@login_required_if_no_ano
def publisher_list():
if current_user.get_view_property('publisher', 'dir') == 'desc':
order = db.Publishers.name.desc()
order_no = 0
else:
order = db.Publishers.name.asc()
order_no = 1
if current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_publishers_link.publisher')).order_by(order).all()
no_publisher_count = (calibre_db.session.query(db.Books)
.outerjoin(db.books_publishers_link).outerjoin(db.Publishers)
.filter(db.Publishers.name == None)
.filter(calibre_db.common_filters())
.count())
if no_publisher_count:
entries.append([db.Category(_("None"), "-1"), no_publisher_count])
entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
char_list = generate_char_list(entries)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_("Publishers"), page="publisherlist", data="publisher", order=order_no)
else:
abort(404)
@web.route("/series")
@login_required_if_no_ano
def series_list():
if current_user.check_visibility(constants.SIDEBAR_SERIES):
if current_user.get_view_property('series', 'dir') == 'desc':
order = db.Series.sort.desc()
order_no = 0
else:
order = db.Series.sort.asc()
order_no = 1
char_list = query_char_list(db.Series.sort, db.books_series_link)
if current_user.get_view_property('series', 'series_view') == 'list':
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(order).all()
no_series_count = (calibre_db.session.query(db.Books)
.outerjoin(db.books_series_link).outerjoin(db.Series)
.filter(db.Series.name == None)
.filter(calibre_db.common_filters())
.count())
if no_series_count:
entries.append([db.Category(_("None"), "-1"), no_series_count])
entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
return render_title_template('list.html',
entries=entries,
folder='web.books_list',
charlist=char_list,
title=_("Series"),
page="serieslist",
data="series", order=order_no)
else:
entries = (calibre_db.session.query(db.Books, func.count('books_series_link').label('count'),
func.max(db.Books.series_index), db.Books.id)
.join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())
.group_by(text('books_series_link.series'))
.having(or_(func.max(db.Books.series_index), db.Books.series_index==""))
.order_by(order)
.all())
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_("Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no)
else:
abort(404)
@web.route("/ratings")
@login_required_if_no_ano
def ratings_list():
if current_user.check_visibility(constants.SIDEBAR_RATING):
if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Ratings.rating.desc()
order_no = 0
else:
order = db.Ratings.rating.asc()
order_no = 1
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.filter(db.Ratings.rating > 0) \
.group_by(text('books_ratings_link.rating')).order_by(order).all()
no_rating_count = (calibre_db.session.query(db.Books)
.outerjoin(db.books_ratings_link).outerjoin(db.Ratings)
.filter(or_(db.Ratings.rating == None, db.Ratings.rating == 0))
.filter(calibre_db.common_filters())
.count())
if no_rating_count:
entries.append([db.Category(_("None"), "-1", -1), no_rating_count])
entries = sorted(entries, key=lambda x: x[0].rating, reverse=not order_no)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_("Ratings list"), page="ratingslist", data="ratings", order=order_no)
else:
abort(404)
@web.route("/formats")
@login_required_if_no_ano
def formats_list():
if current_user.check_visibility(constants.SIDEBAR_FORMAT):
if current_user.get_view_property('formats', 'dir') == 'desc':
order = db.Data.format.desc()
order_no = 0
else:
order = db.Data.format.asc()
order_no = 1
entries = calibre_db.session.query(db.Data,
func.count('data.book').label('count'),
db.Data.format.label('format')) \
.join(db.Books).filter(calibre_db.common_filters()) \
.group_by(db.Data.format).order_by(order).all()
no_format_count = (calibre_db.session.query(db.Books).outerjoin(db.Data)
.filter(db.Data.format == None)
.filter(calibre_db.common_filters())
.count())
if no_format_count:
entries.append([db.Category(_("None"), "-1"), no_format_count])
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_("File formats list"), page="formatslist", data="formats", order=order_no)
else:
abort(404)
@web.route("/language")
@login_required_if_no_ano
def language_overview():
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == "all":
order_no = 0 if current_user.get_view_property('language', 'dir') == 'desc' else 1
languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
char_list = generate_char_list(languages)
return render_title_template('list.html', entries=languages, folder='web.books_list', charlist=char_list,
title=_("Languages"), page="langlist", data="language", order=order_no)
else:
abort(404)
@web.route("/category")
@login_required_if_no_ano
def category_list():
if current_user.check_visibility(constants.SIDEBAR_CATEGORY):
if current_user.get_view_property('category', 'dir') == 'desc':
order = db.Tags.name.desc()
order_no = 0
else:
order = db.Tags.name.asc()
order_no = 1
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(db.Tags.id).all()
no_tag_count = (calibre_db.session.query(db.Books)
.outerjoin(db.books_tags_link).outerjoin(db.Tags)
.filter(db.Tags.name == None)
.filter(calibre_db.common_filters())
.count())
if no_tag_count:
entries.append([db.Category(_("None"), "-1"), no_tag_count])
entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
char_list = generate_char_list(entries)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_("Categories"), page="catlist", data="category", order=order_no)
else:
abort(404)
# ################################### Download/Send ##################################################################
@web.route("/cover/<int:book_id>")
@web.route("/cover/<int:book_id>/<string:resolution>")
@login_required_if_no_ano
def get_cover(book_id, resolution=None):
resolutions = {
'og': constants.COVER_THUMBNAIL_ORIGINAL,
'sm': constants.COVER_THUMBNAIL_SMALL,
'md': constants.COVER_THUMBNAIL_MEDIUM,
'lg': constants.COVER_THUMBNAIL_LARGE,
}
cover_resolution = resolutions.get(resolution, None)
return get_book_cover(book_id, cover_resolution)
@web.route("/series_cover/<int:series_id>")
@web.route("/series_cover/<int:series_id>/<string:resolution>")
@login_required_if_no_ano
def get_series_cover(series_id, resolution=None):
resolutions = {
'og': constants.COVER_THUMBNAIL_ORIGINAL,
'sm': constants.COVER_THUMBNAIL_SMALL,
'md': constants.COVER_THUMBNAIL_MEDIUM,
'lg': constants.COVER_THUMBNAIL_LARGE,
}
cover_resolution = resolutions.get(resolution, None)
return get_series_cover_thumbnail(series_id, cover_resolution)
@web.route("/robots.txt")
def get_robots():
try:
return send_from_directory(constants.STATIC_DIR, "robots.txt")
except PermissionError:
log.error("No permission to access robots.txt file.")
abort(403)
@web.route("/show/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/show/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano
@viewer_required
def serve_book(book_id, book_format, anyname):
book_format = book_format.split(".")[0]
book = calibre_db.get_book(book_id)
data = calibre_db.get_book_format(book_id, book_format.upper())
if not data:
return "File not in Database"
range_header = request.headers.get('Range', None)
if not range_header:
log.info('Serving book: \'%s\' to %s - %s', data.name, current_user.name,
request.headers.get('X-Forwarded-For', request.remote_addr))
if config.config_use_google_drive:
try:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
if not range_header:
headers['Accept-Ranges'] = 'bytes'
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
except AttributeError as ex:
log.error_or_exception(ex)
return "File Not Found"
else:
if book_format.upper() == 'TXT':
try:
rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read()
result = chardet.detect(rawdata)
try:
text_data = rawdata.decode(result['encoding']).encode('utf-8')
except UnicodeDecodeError as e:
log.error("Encoding error in text file {}: {}".format(book.id, e))
if "surrogate" in e.reason:
text_data = rawdata.decode(result['encoding'], 'surrogatepass').encode('utf-8', 'surrogatepass')
else:
text_data = rawdata.decode(result['encoding'], 'ignore').encode('utf-8', 'ignore')
return make_response(text_data)
except FileNotFoundError:
log.error("File Not Found")
return "File Not Found"
# enable byte range read of pdf
response = make_response(
send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format))
if not range_header:
response.headers['Accept-Ranges'] = 'bytes'
return response
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano
@download_required
def download_link(book_id, book_format, anyname):
client = "kobo" if "Kobo" in request.headers.get('User-Agent') else ""
return get_download_link(book_id, book_format, client)
@web.route('/send/<int:book_id>/<book_format>/<int:convert>', methods=["POST"])
@login_required_if_no_ano
@download_required
def send_to_ereader(book_id, book_format, convert):
if not config.get_mail_server_configured():
return make_response(jsonify(type="danger", message=_("Please configure the SMTP mail settings first...")))
elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(),
current_user.name)
if result is None:
ub.update_download(book_id, int(current_user.id))
response = [{'type': "success", 'message': _("Success! Book queued for sending to %(eReadermail)s",
eReadermail=current_user.kindle_mail)}]
else:
response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}]
else:
response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}]
return make_response(jsonify(response))
# ################################### Login Logout ##################################################################
@web.route('/register', methods=['POST'])
@limiter.limit("40/day", key_func=get_remote_address)
@limiter.limit("3/minute", key_func=get_remote_address)
def register_post():
if not config.config_public_reg:
abort(404)
to_save = request.form.to_dict()
try:
limiter.check()
except RateLimitExceeded:
flash(_(u"Please wait one minute to register next user"), category="error")
return render_title_template('register.html', config=config, title=_("Register"), page="register")
except (ConnectionError, Exception) as e:
log.error("Connection error to limiter backend: %s", e)
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
return render_title_template('register.html', config=config, title=_("Register"), page="register")
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if not config.get_mail_server_configured():
flash(_("Oops! Email server is not configured, please contact your administrator."), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
nickname = strip_whitespaces(to_save.get("email", "")) if config.config_register_email else to_save.get('name')
if not nickname or not to_save.get("email"):
flash(_("Oops! Please complete all fields."), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
try:
nickname = check_username(nickname)
email = check_email(to_save.get("email", ""))
except Exception as ex:
flash(str(ex), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
content = ub.User()
if check_valid_domain(email):
content.name = nickname
content.email = email
password = generate_random_password(config.config_password_min_length)
content.password = generate_password_hash(password)
content.role = config.config_default_role
content.locale = config.config_default_locale
content.sidebar_view = config.config_default_show
try:
ub.session.add(content)
ub.session.commit()
if feature_support['oauth']:
register_user_with_oauth(content)
send_registration_mail(strip_whitespaces(to_save.get("email", "")), nickname, password)
except Exception:
ub.session.rollback()
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
else:
flash(_("Oops! Your Email is not allowed."), category="error")
log.warning('Registering failed for user "{}" Email: {}'.format(nickname, to_save.get("email","")))
return render_title_template('register.html', title=_("Register"), page="register")
flash(_("Success! Confirmation Email has been sent."), category="success")
return redirect(url_for('web.login'))
@web.route('/register', methods=['GET'])
def register():
if not config.config_public_reg:
abort(404)
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if not config.get_mail_server_configured():
flash(_("Oops! Email server is not configured, please contact your administrator."), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
if feature_support['oauth']:
register_user_with_oauth()
return render_title_template('register.html', config=config, title=_("Register"), page="register")
def handle_login_user(user, remember, message, category):
login_user(user, remember=remember)
flash(message, category=category)
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
def render_login(username="", password=""):
next_url = request.args.get('next', default=url_for("web.index"), type=str)
if url_for("web.logout") == next_url:
next_url = url_for("web.index")
return render_title_template('login.html',
title=_("Login"),
next_url=next_url,
config=config,
username=username,
password=password,
oauth_check=oauth_check,
mail=config.get_mail_server_configured(), page="login")
@web.route('/login', methods=['GET'])
def login():
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
log.error(u"Cannot activate LDAP authentication")
flash(_(u"Cannot activate LDAP authentication"), category="error")
return render_login()
@web.route('/login', methods=['POST'])
@limiter.limit("40/day", key_func=lambda: strip_whitespaces(request.form.get('username', "")).lower())
@limiter.limit("3/minute", key_func=lambda: strip_whitespaces(request.form.get('username', "")).lower())
def login_post():
form = request.form.to_dict()
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
try:
limiter.check()
except RateLimitExceeded:
flash(_("Please wait one minute before next login"), category="error")
return render_login(username, form.get("password", ""))
except (ConnectionError, Exception) as e:
log.error("Connection error to limiter backend: %s", e)
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
return render_login(username, form.get("password", ""))
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
log.error(u"Cannot activate LDAP authentication")
flash(_(u"Cannot activate LDAP authentication"), category="error")
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username).first()
remember_me = bool(form.get('remember_me'))
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(username, form['password'])
if login_result:
log.debug(u"You are now logged in as: '{}'".format(user.name))
return handle_login_user(user,
remember_me,
_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
"success")
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
and user.name != "Guest":
log.info("Local Fallback Login as: '{}'".format(user.name))
return handle_login_user(user,
remember_me,
_(u"Fallback Login as: '%(nickname)s', "
u"LDAP Server not reachable, or user not known", nickname=user.name),
"warning")
elif login_result is None:
log.info(error)
flash(_(u"Could not login: %(message)s", message=error), category="error")
else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('LDAP Login failed for user "%s" IP-address: %s', username, ip_address)
flash(_(u"Wrong Username or Password"), category="error")
else:
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
if form.get('forgot', "") == 'forgot':
if user is not None and user.name != "Guest":
ret, __ = reset_password(user.id)
if ret == 1:
flash(_(u"New Password was sent to your email address"), category="info")
log.info('Password reset for user "%s" IP-address: %s', username, ip_address)
else:
log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error")
else:
flash(_(u"Please enter valid username to reset password"), category="error")
log.warning('Username missing for password reset IP-address: %s', ip_address)
else:
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
config.config_is_initial = False
log.debug(u"You are now logged in as: '{}'".format(user.name))
return handle_login_user(user,
remember_me,
_(u"You are now logged in as: '%(nickname)s'", nickname=user.name),
"success")
else:
log.warning('Login failed for user "{}" IP-address: {}'.format(username, ip_address))
flash(_(u"Wrong Username or Password"), category="error")
return render_login(username, form.get("password", ""))
@web.route('/logout')
@user_login_required
def logout():
if current_user is not None and current_user.is_authenticated:
ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
logout_user()
if feature_support['oauth'] and (config.config_login_type == 2 or config.config_login_type == 3):
logout_oauth_user()
log.debug("User logged out")
if config.config_anonbrowse:
location = get_redirect_location(request.args.get('next', None), "web.login")
else:
location = None
if location:
return redirect(location)
else:
return redirect(url_for('web.login'))
# ################################### Users own configuration #########################################################
def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages):
to_save = request.form.to_dict()
current_user.random_books = 0
try:
if current_user.role_passwd() or current_user.role_admin():
if to_save.get("password", "") != "":
current_user.password = generate_password_hash(valid_password(to_save.get("password")))
if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail:
current_user.kindle_mail = valid_email(to_save.get("kindle_mail"))
new_email = valid_email(to_save.get("email", current_user.email))
if not new_email:
raise Exception(_("Email can't be empty and has to be a valid Email"))
if new_email != current_user.email:
current_user.email = check_email(new_email)
if current_user.role_admin():
if to_save.get("name", current_user.name) != current_user.name:
# Query username, if not existing, change
current_user.name = check_username(to_save.get("name"))
current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
current_user.default_language = to_save.get("default_language", "all")
current_user.locale = to_save.get("locale", "en")
old_state = current_user.kobo_only_shelves_sync
# 1 -> 0: nothing has to be done
# 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs which
# don't have to be synced have to be removed (added to Shelf archive)
current_user.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
if old_state == 0 and current_user.kobo_only_shelves_sync == 1:
kobo_sync_status.update_on_sync_shelfs(current_user.id)
except Exception as ex:
flash(str(ex), category="error")
return render_title_template("user_edit.html",
content=current_user,
config=config,
translations=translations,
profile=1,
languages=languages,
title=_("%(name)s's Profile", name=current_user.name),
page="me",
kobo_support=kobo_support,
registered_oauth=local_oauth_check,
oauth_status=oauth_status)
val = 0
for key, __ in to_save.items():
if key.startswith('show'):
val += int(key[5:])
current_user.sidebar_view = val
if to_save.get("Show_detail_random"):
current_user.sidebar_view += constants.DETAIL_RANDOM
try:
ub.session.commit()
flash(_("Success! Profile Updated"), category="success")
log.debug("Profile updated")
except IntegrityError:
ub.session.rollback()
flash(_("Oops! An account already exists for this Email."), category="error")
log.debug("Found an existing account for this Email")
except OperationalError as e:
ub.session.rollback()
log.error("Database error: %s", e)
flash(_("Oops! Database Error: %(error)s.", error=e), category="error")
@web.route("/me", methods=["GET", "POST"])
@user_login_required
def profile():
languages = calibre_db.speaking_language()
translations = get_available_locale()
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if feature_support['oauth'] and config.config_login_type == 2:
oauth_status = get_oauth_status()
local_oauth_check = oauth_check
else:
oauth_status = None
local_oauth_check = {}
if request.method == "POST":
change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages)
return render_title_template("user_edit.html",
translations=translations,
profile=1,
languages=languages,
content=current_user,
config=config,
kobo_support=kobo_support,
title=_("%(name)s's Profile", name=current_user.name),
page="me",
registered_oauth=local_oauth_check,
oauth_status=oauth_status)
# ###################################Show single book ##################################################################
@web.route("/read/<int:book_id>/<book_format>")
@login_required_if_no_ano
@viewer_required
def read_book(book_id, book_format):
book = calibre_db.get_filtered_book(book_id)
if not book:
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
log.debug("Selected book is unavailable. File does not exist or is not accessible")
return redirect(url_for("web.index"))
book.ordered_authors = calibre_db.order_authors([book], False)
# check if book has a bookmark
bookmark = None
if current_user.is_authenticated:
bookmark = ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
ub.Bookmark.book_id == book_id,
ub.Bookmark.format == book_format.upper())).first()
if book_format.lower() == "epub" or book_format.lower() == "kepub":
log.debug("Start [k]epub reader for %d", book_id)
return render_title_template('read.html', bookid=book_id, title=book.title, bookmark=bookmark,
book_format=book_format)
elif book_format.lower() == "pdf":
log.debug("Start pdf reader for %d", book_id)
return render_title_template('readpdf.html', pdffile=book_id, title=book.title)
elif book_format.lower() == "txt":
log.debug("Start txt reader for %d", book_id)
return render_title_template('readtxt.html', txtfile=book_id, title=book.title)
elif book_format.lower() in ["djvu", "djv"]:
log.debug("Start djvu reader for %d", book_id)
return render_title_template('readdjvu.html', djvufile=book_id, title=book.title,
extension=book_format.lower())
else:
for fileExt in constants.EXTENSIONS_AUDIO:
if book_format.lower() == fileExt:
entries = calibre_db.get_filtered_book(book_id)
log.debug("Start mp3 listening for %d", book_id)
return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(),
entry=entries, bookmark=bookmark)
for fileExt in ["cbr", "cbt", "cbz"]:
if book_format.lower() == fileExt:
all_name = str(book_id)
title = book.title
if len(book.series):
title = title + " - " + book.series[0].name
if book.series_index:
title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.')
log.debug("Start comic reader for %d", book_id)
return render_title_template('readcbr.html', comicfile=all_name, title=title,
extension=fileExt, bookmark=bookmark)
log.debug("Selected book is unavailable. File does not exist or is not accessible")
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
@web.route("/book/<int:book_id>")
@login_required_if_no_ano
def show_book(book_id):
entries = calibre_db.get_book_read_archived(book_id, config.config_read_column, allow_show_archived=True)
if entries:
read_book = entries[1]
archived_book = entries[2]
entry = entries[0]
entry.read_status = read_book == ub.ReadBook.STATUS_FINISHED
entry.is_archived = archived_book
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 = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
book_in_shelves = []
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)
entry.ordered_authors = calibre_db.order_authors([entry])
entry.email_share_list = check_send_to_ereader(entry)
entry.reader_list = check_read_formats(entry)
entry.reader_list_sizes = dict()
for data in entry.data:
if data.format.lower() in entry.reader_list:
entry.reader_list_sizes[data.format.lower()] = data.uncompressed_size
entry.audio_entries = []
for media_format in entry.data:
if media_format.format.lower() in constants.EXTENSIONS_AUDIO:
entry.audio_entries.append(media_format.format.lower())
return render_title_template('detail.html',
entry=entry,
cc=cc,
is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
title=entry.title,
books_shelfs=book_in_shelves,
page="book")
else:
log.debug("Selected book is unavailable. File does not exist or is not accessible")
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))