Better epub cover parsing with multiple cover-image items

Code cosmetics
renamed variables
refactored xml page generation
refactored prepare author
pull/2347/head
Ozzie Isaacs 2 years ago
parent 296f76b5fb
commit 4545f4a20d

@ -40,7 +40,7 @@ from cps.about import about
from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
from cps.editbooks import EditBook
from cps.remotelogin import remotelogin
from cps.search_metadata import meta
from cps.error_handler import init_errorhandler
@ -73,7 +73,7 @@ def main():
app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
app.register_blueprint(EditBook)
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)

@ -156,7 +156,7 @@ def create_app():
services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret,
config.config_use_goodreads)
config.store_calibre_uuid(calibre_db, db.Library_Id)
config.store_calibre_uuid(calibre_db, db.LibraryId)
return app
@babel.localeselector

@ -27,8 +27,9 @@ import json
import time
import operator
from datetime import datetime, timedelta
from functools import wraps
from babel import Locale as LC
from babel import Locale
from babel.dates import format_datetime
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask_login import login_required, current_user, logout_user, confirm_login
@ -47,7 +48,6 @@ from .gdriveutils import is_gdrive_ready, gdrive_support
from .render_template import render_title_template, get_sidebar_config
from . import debug_info, _BABEL_TRANSLATIONS
from functools import wraps
log = logger.create()
@ -189,10 +189,10 @@ def admin():
else:
commit = version['version']
allUser = ub.session.query(ub.User).all()
all_user = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings()
kobo_support = feature_support['kobo'] and config.config_kobo_sync
return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit,
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
feature_support=feature_support, kobo_support=kobo_support,
title=_(u"Admin page"), page="admin")
@ -242,12 +242,12 @@ def calibreweb_alive():
@login_required
@admin_required
def view_configuration():
read_column = calibre_db.session.query(db.Custom_Columns)\
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all()
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all()
read_column = calibre_db.session.query(db.CustomColumns)\
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all()
restrict_columns = calibre_db.session.query(db.CustomColumns)\
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all()
languages = calibre_db.speaking_language()
translations = [LC('en')] + babel.list_translations()
translations = [Locale('en')] + babel.list_translations()
return render_title_template("config_view_edit.html", conf=config, readColumns=read_column,
restrictColumns=restrict_columns,
languages=languages,
@ -261,8 +261,8 @@ def view_configuration():
def edit_user_table():
visibility = current_user.view_settings.get('useredit', {})
languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')]
allUser = ub.session.query(ub.User)
translations = babel.list_translations() + [Locale('en')]
all_user = ub.session.query(ub.User)
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
@ -274,10 +274,10 @@ def edit_user_table():
else:
custom_values = []
if not config.config_anonbrowse:
allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
kobo_support = feature_support['kobo'] and config.config_kobo_sync
return render_title_template("user_table.html",
users=allUser.all(),
users=all_user.all(),
tags=tags,
custom_values=custom_values,
translations=translations,
@ -332,7 +332,7 @@ def list_users():
if user.default_language == "all":
user.default = _("All")
else:
user.default = LC.parse(user.default_language).get_language_name(get_locale())
user.default = Locale.parse(user.default_language).get_language_name(get_locale())
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
@ -380,7 +380,7 @@ def delete_user():
@login_required
@admin_required
def table_get_locale():
locale = babel.list_translations() + [LC('en')]
locale = babel.list_translations() + [Locale('en')]
ret = list()
current_locale = get_locale()
for loc in locale:
@ -444,7 +444,7 @@ def edit_list_user(param):
elif param.endswith('role'):
value = int(vals['field_index'])
if user.name == "Guest" and value in \
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
raise Exception(_("Guest can't have this role"))
# check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1):
@ -524,16 +524,16 @@ def update_table_settings():
def check_valid_read_column(column):
if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all():
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
return False
return True
def check_valid_restricted_column(column):
if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all():
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
return False
return True
@ -1078,12 +1078,12 @@ def _configuration_oauth_helper(to_save):
reboot_required = False
for element in oauthblueprints:
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
if to_save["config_" + str(element['id']) + "_oauth_client_id"] \
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
active_oauths += 1
element["active"] = 1
else:
@ -1136,7 +1136,7 @@ def _configuration_ldap_helper(to_save):
if not config.config_ldap_provider_url \
or not config.config_ldap_port \
or not config.config_ldap_dn \
or not config.config_ldap_user_object:
or not config.config_ldap_user_object:
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'))
@ -1211,6 +1211,7 @@ def _db_configuration_update_helper():
'',
to_save['config_calibre_dir'],
flags=re.IGNORECASE)
db_valid = False
try:
db_change, db_valid = _db_simulate_change()
@ -1229,11 +1230,11 @@ def _db_configuration_update_helper():
return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured \
or config.config_calibre_dir != to_save["config_calibre_dir"]:
or config.config_calibre_dir != to_save["config_calibre_dir"]:
if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path):
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error)
config.store_calibre_uuid(calibre_db, db.Library_Id)
config.store_calibre_uuid(calibre_db, db.LibraryId)
# if db changed -> delete shelfs, delete download books, delete read books, kobo sync...
if db_change:
log.info("Calibre Database changed, delete all Calibre-Web info related to old Database")
@ -1272,7 +1273,7 @@ def _configuration_update_helper():
_config_checkbox_int(to_save, "config_unicode_filename")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
and config.config_login_type == constants.LOGIN_LDAP)
and config.config_login_type == constants.LOGIN_LDAP)
_config_checkbox_int(to_save, "config_public_reg")
_config_checkbox_int(to_save, "config_register_email")
reboot_required |= _config_checkbox_int(to_save, "config_kobo_sync")
@ -1560,7 +1561,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
def new_user():
content = ub.User()
languages = calibre_db.speaking_language()
translations = [LC('en')] + babel.list_translations()
translations = [Locale('en')] + babel.list_translations()
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if request.method == "POST":
to_save = request.form.to_dict()
@ -1647,7 +1648,7 @@ def edit_user(user_id):
flash(_(u"User not found"), category="error")
return redirect(url_for('admin.admin'))
languages = calibre_db.speaking_language(return_all_languages=True)
translations = babel.list_translations() + [LC('en')]
translations = babel.list_translations() + [Locale('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if request.method == "POST":
to_save = request.form.to_dict()

@ -17,13 +17,13 @@
# 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 copy
import os
import re
import ast
import json
from datetime import datetime
from urllib.parse import quote
import unidecode
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
@ -49,11 +49,6 @@ from .pagination import Pagination
from weakref import WeakSet
try:
import unidecode
use_unidecode = True
except ImportError:
use_unidecode = False
log = logger.create()
@ -93,7 +88,7 @@ books_publishers_link = Table('books_publishers_link', Base.metadata,
)
class Library_Id(Base):
class LibraryId(Base):
__tablename__ = 'library_id'
id = Column(Integer, primary_key=True)
uuid = Column(String, nullable=False)
@ -112,7 +107,7 @@ class Identifiers(Base):
self.type = id_type
self.book = book
def formatType(self):
def format_type(self):
format_type = self.type.lower()
if format_type == 'amazon':
return u"Amazon"
@ -184,8 +179,8 @@ class Comments(Base):
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
text = Column(String(collation='NOCASE'), nullable=False)
def __init__(self, text, book):
self.text = text
def __init__(self, comment, book):
self.text = comment
self.book = book
def get(self):
@ -367,7 +362,6 @@ class Books(Base):
self.path = path
self.has_cover = (has_cover != None)
def __repr__(self):
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
self.timestamp, self.pubdate, self.series_index,
@ -375,10 +369,10 @@ class Books(Base):
@property
def atom_timestamp(self):
return (self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '')
return self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or ''
class Custom_Columns(Base):
class CustomColumns(Base):
__tablename__ = 'custom_columns'
id = Column(Integer, primary_key=True)
@ -436,7 +430,7 @@ class AlchemyEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
class CalibreDB():
class CalibreDB:
_init = False
engine = None
config = None
@ -450,17 +444,17 @@ class CalibreDB():
"""
self.session = None
if self._init:
self.initSession(expire_on_commit)
self.init_session(expire_on_commit)
self.instances.add(self)
def initSession(self, expire_on_commit=True):
def init_session(self, expire_on_commit=True):
self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config)
@classmethod
def setup_db_cc_classes(self, cc):
def setup_db_cc_classes(cls, cc):
cc_ids = []
books_custom_column_links = {}
for row in cc:
@ -539,16 +533,16 @@ class CalibreDB():
return False, False
try:
check_engine = create_engine('sqlite://',
echo=False,
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False},
poolclass=StaticPool)
echo=False,
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False},
poolclass=StaticPool)
with check_engine.begin() as connection:
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
local_session = scoped_session(sessionmaker())
local_session.configure(bind=connection)
database_uuid = local_session().query(Library_Id).one_or_none()
database_uuid = local_session().query(LibraryId).one_or_none()
# local_session.dispose()
check_engine.connect()
@ -603,7 +597,7 @@ class CalibreDB():
autoflush=True,
bind=cls.engine))
for inst in cls.instances:
inst.initSession()
inst.init_session()
cls._init = True
return True
@ -644,12 +638,10 @@ class CalibreDB():
# Language and content filters for displaying in the UI
def common_filters(self, allow_show_archived=False, return_all_languages=False):
if not allow_show_archived:
archived_books = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.is_archived == True)
.all()
)
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 = Books.id.notin_(archived_book_ids)
else:
@ -668,11 +660,11 @@ class CalibreDB():
pos_cc_list = current_user.allowed_column_value.split(',')
pos_content_cc_filter = true() if pos_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
neg_cc_list = current_user.denied_column_value.split(',')
neg_content_cc_filter = false() if neg_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
except (KeyError, AttributeError):
pos_content_cc_filter = false()
neg_content_cc_filter = true()
@ -728,7 +720,7 @@ class CalibreDB():
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)))
and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == Books.id)))
else:
try:
read_column = cc_classes[config_read_column]
@ -738,7 +730,7 @@ class CalibreDB():
except (KeyError, AttributeError):
log.error("Custom Column No.%d is not existing in calibre database", read_column)
# Skip linking read column and return None instead of read status
query =self.session.query(database, None, ub.ArchivedBook.is_archived)
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))
else:
@ -812,7 +804,6 @@ class CalibreDB():
return authors_ordered
return entries
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
query = query or ''
self.session.connection().connection.connection.create_function("lower", 1, lcase)
@ -872,7 +863,7 @@ class CalibreDB():
))
# 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, allow_show_archived=False,
def get_search_results(self, term, offset=None, order=None, limit=None,
config_read_column=False, *join):
order = order[0] if order else [Books.sort]
pagination = None
@ -915,7 +906,6 @@ class CalibreDB():
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
def update_title_sort(self, config, conn=None):
# user defined sort function for calibre databases (Series, etc.)
def _title_sort(title):
@ -973,6 +963,6 @@ def lcase(s):
try:
return unidecode.unidecode(s.lower())
except Exception as ex:
log = logger.create()
log.error_or_exception(ex)
_log = logger.create()
_log.error_or_exception(ex)
return s.lower()

@ -31,7 +31,7 @@ from functools import wraps
try:
from lxml.html.clean import clean_html
except ImportError:
pass
clean_html = None
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _
@ -48,7 +48,7 @@ from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import change_archived_books
editbook = Blueprint('editbook', __name__)
EditBook = Blueprint('edit-book', __name__)
log = logger.create()
@ -61,6 +61,7 @@ def upload_required(f):
return inner
def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
@ -70,6 +71,7 @@ def edit_required(f):
return inner
def search_objects_remove(db_book_object, db_type, input_elements):
del_elements = []
for c_elements in db_book_object:
@ -119,6 +121,7 @@ def remove_objects(db_book_object, db_session, del_elements):
db_session.delete(del_element)
return changed
def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
changed = False
if db_type == 'languages':
@ -128,7 +131,7 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
else:
db_filter = db_object.name
for add_element in add_elements:
# check if a element with that name exists
# check if an element with that name exists
db_element = db_session.query(db_object).filter(db_filter == add_element).first()
# if no element is found add it
if db_type == 'author':
@ -147,7 +150,6 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
db_book_object.append(new_element)
else:
db_element = create_objects_for_addition(db_element, add_element, db_type)
changed = True
# add element to book
changed = True
db_book_object.append(db_element)
@ -178,7 +180,7 @@ def create_objects_for_addition(db_element, add_element, db_type):
return db_element
# Modifies different Database objects, first check if elements if elements have to be deleted,
# Modifies different Database objects, first check if elements have to be deleted,
# because they are no longer used, than check if elements have to be added to database
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
# passing input_elements not as a list may lead to undesired results
@ -207,7 +209,7 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
input_dict = dict([(identifier.type.lower(), identifier) for identifier in input_identifiers])
if len(input_identifiers) != len(input_dict):
error = True
db_dict = dict([(identifier.type.lower(), identifier) for identifier in db_identifiers ])
db_dict = dict([(identifier.type.lower(), identifier) for identifier in db_identifiers])
# delete db identifiers not present in input or modify them with input val
for identifier_type, identifier in db_dict.items():
if identifier_type not in input_dict.keys():
@ -224,14 +226,15 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True
return changed, error
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@EditBook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@login_required
def delete_book_from_details(book_id):
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@EditBook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@EditBook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@login_required
def delete_book_ajax(book_id, book_format):
return delete_book_from_table(book_id, book_format, False)
@ -252,8 +255,8 @@ def delete_whole_book(book_id, book):
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
cc = calibre_db.session.query(db.Custom_Columns). \
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
cc = calibre_db.session.query(db.CustomColumns). \
filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
for c in cc:
cc_string = "custom_column_" + str(c.id)
if not c.is_multiple:
@ -283,18 +286,18 @@ def delete_whole_book(book_id, book):
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
def render_delete_book_result(book_format, jsonResponse, warning, book_id):
def render_delete_book_result(book_format, json_response, warning, book_id):
if book_format:
if jsonResponse:
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
if json_response:
return json.dumps([warning, {"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "success",
"format": book_format,
"message": _('Book Format Successfully Deleted')}])
else:
flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
else:
if jsonResponse:
if json_response:
return json.dumps([warning, {"location": url_for('web.index'),
"type": "success",
"format": book_format,
@ -304,7 +307,7 @@ def render_delete_book_result(book_format, jsonResponse, warning, book_id):
return redirect(url_for('web.index'))
def delete_book_from_table(book_id, book_format, jsonResponse):
def delete_book_from_table(book_id, book_format, json_response):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
@ -312,20 +315,20 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}])
if json_response:
return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}])
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book", book_id=book_id),
"type": "warning",
"format": "",
"message": error}
if json_response:
warning = {"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "warning",
"format": "",
"message": error}
else:
flash(error, category="warning")
if not book_format:
@ -339,35 +342,36 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
except Exception as ex:
log.error_or_exception(ex)
calibre_db.session.rollback()
if jsonResponse:
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
if json_response:
return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}])
else:
flash(str(ex), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
return render_delete_book_result(book_format, json_response, warning, book_id)
message = _("You are missing permissions to delete books")
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book", book_id=book_id),
if json_response:
return json.dumps({"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": message})
else:
flash(message, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
def render_edit_book(book_id):
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
for lang in book.languages:
@ -380,9 +384,9 @@ def render_edit_book(book_id):
author_names.append(authr.name.replace('|', ','))
# Option for showing convertbook button
valid_source_formats=list()
valid_source_formats = list()
allowed_conversion_formats = list()
kepub_possible=None
kepub_possible = None
if config.config_converterpath:
for file in book.data:
if file.format.lower() in constants.EXTENSIONS_CONVERT_FROM:
@ -430,6 +434,7 @@ def edit_book_ratings(to_save, book):
changed = True
return changed
def edit_book_tags(tags, book):
input_tags = tags.split(',')
input_tags = list(map(lambda it: it.strip(), input_tags))
@ -446,48 +451,48 @@ def edit_book_series(series, book):
def edit_book_series_index(series_index, book):
# Add default series_index to book
modif_date = False
modify_date = False
series_index = series_index or '1'
if not series_index.replace('.', '', 1).isdigit():
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
return False
if str(book.series_index) != series_index:
book.series_index = series_index
modif_date = True
return modif_date
modify_date = True
return modify_date
# Handle book comments/description
def edit_book_comments(comments, book):
modif_date = False
modify_date = False
if comments:
comments = clean_html(comments)
if len(book.comments):
if book.comments[0].text != comments:
book.comments[0].text = comments
modif_date = True
modify_date = True
else:
if comments:
book.comments.append(db.Comments(text=comments, book=book.id))
modif_date = True
return modif_date
book.comments.append(db.Comments(comment=comments, book=book.id))
modify_date = True
return modify_date
def edit_book_languages(languages, book, upload=False, invalid=None):
def edit_book_languages(languages, book, upload_mode=False, invalid=None):
input_languages = languages.split(',')
unknown_languages = []
if not upload:
if not upload_mode:
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
else:
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages:
log.error("'%s' is not a valid language", l)
for lang in unknown_languages:
log.error("'%s' is not a valid language", lang)
if isinstance(invalid, list):
invalid.append(l)
invalid.append(lang)
else:
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=l))
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=lang))
# ToDo: Not working correct
if upload and len(input_l) == 1:
if upload_mode and len(input_l) == 1:
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
# the book it's language is set to the filter language
if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all":
@ -571,17 +576,20 @@ def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
getattr(book, cc_string).append(new_cc)
return changed, to_save
def edit_single_cc_data(book_id, book, column_id, to_save):
cc = (calibre_db.session.query(db.Custom_Columns)
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions))
.filter(db.Custom_Columns.id == column_id)
cc = (calibre_db.session.query(db.CustomColumns)
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions))
.filter(db.CustomColumns.id == column_id)
.all())
return edit_cc_data(book_id, book, to_save, cc)
def edit_all_cc_data(book_id, book, to_save):
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
return edit_cc_data(book_id, book, to_save, cc)
def edit_cc_data(book_id, book, to_save, cc):
changed = False
for c in cc:
@ -614,10 +622,11 @@ def edit_cc_data(book_id, book, to_save, cc):
'custom')
return changed
def upload_single_file(request, book, book_id):
def upload_single_file(file_request, book, book_id):
# Check and handle Uploaded file
if 'btn-upload-format' in request.files:
requested_file = request.files['btn-upload-format']
if 'btn-upload-format' in file_request.files:
requested_file = file_request.files['btn-upload-format']
# check for empty request
if requested_file.filename != '':
if not current_user.role_upload():
@ -669,17 +678,17 @@ def upload_single_file(request, book, book_id):
# Queue uploader info
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
WorkerThread.add(current_user.name, TaskUpload(uploadText, escape(book.title)))
upload_text = _(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename),
rarExecutable=config.config_rarfile_location)
def upload_cover(request, book):
if 'btn-upload-cover' in request.files:
requested_file = request.files['btn-upload-cover']
def upload_cover(cover_request, book):
if 'btn-upload-cover' in cover_request.files:
requested_file = cover_request.files['btn-upload-cover']
# check for empty request
if requested_file.filename != '':
if not current_user.role_upload():
@ -706,8 +715,8 @@ def handle_title_on_edit(book, book_title):
def handle_author_on_edit(book, author_name, update_stored=True):
# handle author(s)
# renamed = False
input_authors = author_name.split('&')
input_authors, renamed = prepare_authors(author_name)
'''input_authors = author_name.split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
# Remove duplicates in authors list
input_authors = helper.uniq(input_authors)
@ -725,7 +734,7 @@ def handle_author_on_edit(book, author_name, update_stored=True):
sorted_renamed_author = helper.get_sorted_author(renamed_author.name)
sorted_old_author = helper.get_sorted_author(in_aut)
for one_book in all_books:
one_book.author_sort = one_book.author_sort.replace(sorted_renamed_author, sorted_old_author)
one_book.author_sort = one_book.author_sort.replace(sorted_renamed_author, sorted_old_author)'''
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
@ -746,11 +755,11 @@ def handle_author_on_edit(book, author_name, update_stored=True):
return input_authors, change, renamed
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
@EditBook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
@login_required_if_no_ano
@edit_required
def edit_book(book_id):
modif_date = False
modify_date = False
# create the function for sorting...
try:
@ -767,13 +776,14 @@ def edit_book(book_id):
# Book not found
if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
meta = upload_single_file(request, book, book_id)
if upload_cover(request, book) is True:
book.has_cover = 1
modif_date = True
modify_date = True
try:
to_save = request.form.to_dict()
merge_metadata(to_save, meta)
@ -786,15 +796,15 @@ def edit_book(book_id):
input_authors, authorchange, renamed = handle_author_on_edit(book, to_save["author_name"])
if authorchange or title_change:
edited_books_id = book.id
modif_date = True
modify_date = True
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
error = False
error = ""
if edited_books_id:
error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0],
renamed_author=renamed)
renamed_author=renamed)
if not error:
if "cover_url" in to_save:
@ -808,32 +818,32 @@ def edit_book(book_id):
result, error = helper.save_cover_from_url(to_save["cover_url"], book.path)
if result is True:
book.has_cover = 1
modif_date = True
modify_date = True
else:
flash(error, category="error")
# Add default series_index to book
modif_date |= edit_book_series_index(to_save["series_index"], book)
modify_date |= edit_book_series_index(to_save["series_index"], book)
# Handle book comments/description
modif_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
# Handle identifiers
input_identifiers = identifier_list(to_save, book)
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
if warning:
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
modif_date |= modification
modify_date |= modification
# Handle book tags
modif_date |= edit_book_tags(to_save['tags'], book)
modify_date |= edit_book_tags(to_save['tags'], book)
# Handle book series
modif_date |= edit_book_series(to_save["series"], book)
modify_date |= edit_book_series(to_save["series"], book)
# handle book publisher
modif_date |= edit_book_publisher(to_save['publisher'], book)
modify_date |= edit_book_publisher(to_save['publisher'], book)
# handle book languages
modif_date |= edit_book_languages(to_save['languages'], book)
modify_date |= edit_book_languages(to_save['languages'], book)
# handle book ratings
modif_date |= edit_book_ratings(to_save, book)
modify_date |= edit_book_ratings(to_save, book)
# handle cc data
modif_date |= edit_all_cc_data(book_id, book, to_save)
modify_date |= edit_all_cc_data(book_id, book, to_save)
if to_save["pubdate"]:
try:
@ -843,7 +853,7 @@ def edit_book(book_id):
else:
book.pubdate = db.Books.DEFAULT_PUBDATE
if modif_date:
if modify_date:
book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id, all=True)
@ -905,14 +915,7 @@ def identifier_list(to_save, book):
return result
def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'):
entry = calibre_db.check_exists_book(authr, title)
if entry:
log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
def prepare_authors(authr):
# handle authors
input_authors = authr.split('&')
# handle_authors(input_authors)
@ -935,6 +938,18 @@ def prepare_authors_on_upload(title, authr):
sorted_old_author = helper.get_sorted_author(in_aut)
for one_book in all_books:
one_book.author_sort = one_book.author_sort.replace(sorted_renamed_author, sorted_old_author)
return input_authors, renamed
def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'):
entry = calibre_db.check_exists_book(authr, title)
if entry:
log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
input_authors, renamed = prepare_authors(authr)
sort_authors_list = list()
db_author = None
@ -955,7 +970,7 @@ def prepare_authors_on_upload(title, authr):
return sort_authors, input_authors, db_author, renamed
def create_book_on_upload(modif_date, meta):
def create_book_on_upload(modify_date, meta):
title = meta.title
authr = meta.author
sort_authors, input_authors, db_author, renamed_authors = prepare_authors_on_upload(title, authr)
@ -963,34 +978,34 @@ def create_book_on_upload(modif_date, meta):
title_dir = helper.get_valid_filename(title, chars=96)
author_dir = helper.get_valid_filename(db_author.name, chars=96)
# combine path and normalize path from windows systems
# combine path and normalize path from Windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
'author')
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
'author')
# Add series_index to book
modif_date |= edit_book_series_index(meta.series_id, db_book)
modify_date |= edit_book_series_index(meta.series_id, db_book)
# add languages
invalid=[]
modif_date |= edit_book_languages(meta.languages, db_book, upload=True, invalid=invalid)
invalid = []
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
if invalid:
for l in invalid:
flash(_(u"'%(langname)s' is not a valid language", langname=l), category="warning")
for lang in invalid:
flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning")
# handle tags
modif_date |= edit_book_tags(meta.tags, db_book)
modify_date |= edit_book_tags(meta.tags, db_book)
# handle publisher
modif_date |= edit_book_publisher(meta.publisher, db_book)
modify_date |= edit_book_publisher(meta.publisher, db_book)
# handle series
modif_date |= edit_book_series(meta.series, db_book)
modify_date |= edit_book_series(meta.series, db_book)
# Add file to book
file_size = os.path.getsize(meta.file_path)
@ -1002,6 +1017,7 @@ def create_book_on_upload(modif_date, meta):
calibre_db.session.flush()
return db_book, input_authors, title_dir, renamed_authors
def file_handling_on_upload(requested_file):
# check if file extension is correct
if '.' in requested_file.filename:
@ -1045,7 +1061,7 @@ def move_coverfile(meta, db_book):
category="error")
@editbook.route("/upload", methods=["POST"])
@EditBook.route("/upload", methods=["POST"])
@login_required_if_no_ano
@upload_required
def upload():
@ -1054,7 +1070,7 @@ def upload():
if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"):
try:
modif_date = False
modify_date = False
# create the function for sorting...
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
@ -1063,10 +1079,10 @@ def upload():
if error:
return error
db_book, input_authors, title_dir, renamed_authors = create_book_on_upload(modif_date, meta)
db_book, input_authors, title_dir, renamed_authors = create_book_on_upload(modify_date, meta)
# Comments needs book id therefore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
# Comments need book id therefore only possible after flush
modify_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
book_id = db_book.id
title = db_book.title
@ -1096,12 +1112,12 @@ def upload():
if error:
flash(error, category="error")
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
uploadText = _(u"File %(file)s uploaded", file=link)
WorkerThread.add(current_user.name, TaskUpload(uploadText, escape(title)))
upload_text = _(u"File %(file)s uploaded", file=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('editbook.edit_book', book_id=book_id)}
resp = {"location": url_for('edit-book.edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
else:
resp = {"location": url_for('web.show_book', book_id=book_id)}
@ -1113,7 +1129,7 @@ def upload():
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@EditBook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@login_required_if_no_ano
@edit_required
def convert_bookformat(book_id):
@ -1123,7 +1139,7 @@ def convert_bookformat(book_id):
if (book_format_from is None) or (book_format_to is None):
flash(_(u"Source or destination format for conversion missing"), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
@ -1131,31 +1147,33 @@ def convert_bookformat(book_id):
if rtn is None:
flash(_(u"Book successfully queued for converting to %(book_format)s",
book_format=book_format_to),
category="success")
book_format=book_format_to),
category="success")
else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
return redirect(url_for('edit-book.edit_book', book_id=book_id))
@editbook.route("/ajax/getcustomenum/<int:c_id>")
@EditBook.route("/ajax/getcustomenum/<int:c_id>")
@login_required
def table_get_custom_enum(c_id):
ret = list()
cc = (calibre_db.session.query(db.Custom_Columns)
.filter(db.Custom_Columns.id == c_id)
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none())
cc = (calibre_db.session.query(db.CustomColumns)
.filter(db.CustomColumns.id == c_id)
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).one_or_none())
ret.append({'value': "", 'text': ""})
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
ret.append({'value': en, 'text': en})
return json.dumps(ret)
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@EditBook.route("/ajax/editbooks/<param>", methods=['POST'])
@login_required_if_no_ano
@edit_required
def edit_list_book(param):
vals = request.form.to_dict()
book = calibre_db.get_book(vals['pk'])
sort_param = ""
# ret = ""
try:
if param == 'series_index':
@ -1172,7 +1190,7 @@ def edit_list_book(param):
elif param == 'publishers':
edit_book_publisher(vals['value'], book)
ret = Response(json.dumps({'success': True,
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
mimetype='application/json')
elif param == 'languages':
invalid = list()
@ -1186,13 +1204,13 @@ def edit_list_book(param):
for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json')
mimetype='application/json')
elif param == 'author_sort':
book.author_sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
mimetype='application/json')
elif param == 'title':
sort = book.sort
sort_param = book.sort
handle_title_on_edit(book, vals.get('value', ""))
helper.update_dir_structure(book.id, config.config_calibre_dir)
ret = Response(json.dumps({'success': True, 'newValue': book.title}),
@ -1208,12 +1226,13 @@ def edit_list_book(param):
elif param == 'authors':
input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], renamed_author=renamed)
ret = Response(json.dumps({'success': True,
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
mimetype='application/json')
ret = Response(json.dumps({
'success': True,
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
mimetype='application/json')
elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archivebit set to: {}".format(book.id, vals['value']))
message="Book {} archive bit set to: {}".format(book.id, vals['value']))
if is_archived:
kobo_sync_status.remove_synced_book(book.id)
return ""
@ -1238,7 +1257,7 @@ def edit_list_book(param):
calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == "false":
book.sort = sort
book.sort = sort_param
calibre_db.session.commit()
except (OperationalError, IntegrityError) as e:
calibre_db.session.rollback()
@ -1249,7 +1268,7 @@ def edit_list_book(param):
return ret
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
@EditBook.route("/ajax/sort_value/<field>/<int:bookid>")
@login_required
def get_sorted_entry(field, bookid):
if field in ['title', 'authors', 'sort', 'author_sort']:
@ -1266,7 +1285,7 @@ def get_sorted_entry(field, bookid):
return ""
@editbook.route("/ajax/simulatemerge", methods=['POST'])
@EditBook.route("/ajax/simulatemerge", methods=['POST'])
@login_required
@edit_required
def simulate_merge_list_book():
@ -1282,7 +1301,7 @@ def simulate_merge_list_book():
return ""
@editbook.route("/ajax/mergebooks", methods=['POST'])
@EditBook.route("/ajax/mergebooks", methods=['POST'])
@login_required
@edit_required
def merge_list_book():
@ -1295,8 +1314,9 @@ def merge_list_book():
if to_book:
for file in to_book.data:
to_file.append(file.format)
to_name = helper.get_valid_filename(to_book.title, chars=96) + ' - ' + \
helper.get_valid_filename(to_book.authors[0].name, chars=96)
to_name = helper.get_valid_filename(to_book.title,
chars=96) + ' - ' + helper.get_valid_filename(to_book.authors[0].name,
chars=96)
for book_id in vals:
from_book = calibre_db.get_book(book_id)
if from_book:
@ -1314,19 +1334,20 @@ def merge_list_book():
element.format,
element.uncompressed_size,
to_name))
delete_book_from_table(from_book.id,"", True)
delete_book_from_table(from_book.id, "", True)
return json.dumps({'success': True})
return ""
@editbook.route("/ajax/xchange", methods=['POST'])
@EditBook.route("/ajax/xchange", methods=['POST'])
@login_required
@edit_required
def table_xchange_author_title():
vals = request.get_json().get('xchange')
edited_books_id = False
if vals:
for val in vals:
modif_date = False
modify_date = False
book = calibre_db.get_book(val)
authors = book.title
book.authors = calibre_db.order_authors([book])
@ -1338,15 +1359,15 @@ def table_xchange_author_title():
input_authors, authorchange, renamed = handle_author_on_edit(book, authors)
if authorchange or title_change:
edited_books_id = book.id
modif_date = True
modify_date = True
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
if edited_books_id:
helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0],
renamed_author=renamed)
if modif_date:
renamed_author=renamed)
if modify_date:
book.last_modified = datetime.utcnow()
try:
calibre_db.session.commit()

@ -53,11 +53,11 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
txt = epub_zip.read('META-INF/container.xml')
tree = etree.fromstring(txt)
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epub_zip.read(cfname)
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epub_zip.read(cf_name)
tree = etree.fromstring(cf)
coverpath = os.path.dirname(cfname)
cover_path = os.path.dirname(cf_name)
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
@ -90,7 +90,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
epub_metadata = parse_epub_series(ns, tree, epub_metadata)
cover_file = parse_epub_cover(ns, tree, epub_zip, coverpath, tmp_file_path)
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
if not epub_metadata['title']:
title = original_file_name
@ -114,9 +114,12 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
cover_file = None
if len(cover_section) > 0:
cover_file = _extract_cover(epub_zip, cover_section[0], cover_path, tmp_file_path)
else:
# if len(cover_section) > 0:
for cs in cover_section:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
break
if not cover_file:
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
cover_section = tree.xpath(
@ -143,8 +146,7 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
break
if cover_file: break
return cover_file

@ -23,11 +23,10 @@ import mimetypes
import re
import shutil
import socket
import unicodedata
from datetime import datetime, timedelta
from tempfile import gettempdir
from urllib.parse import urlparse
import requests
import unidecode
from babel.dates import format_datetime
from babel.units import format_unit
@ -41,15 +40,19 @@ from werkzeug.security import generate_password_hash
from markupsafe import escape
from urllib.parse import quote
try:
import unidecode
use_unidecode = True
import advocate
from advocate.exceptions import UnacceptableAddressException
use_advocate = True
except ImportError:
use_unidecode = False
use_advocate = False
advocate = requests
UnacceptableAddressException = MissingSchema = BaseException
from . import calibre_db, cli
from .tasks.convert import TaskConvert
from . import logger, config, get_locale, db, ub, kobo_sync_status
from . import logger, config, get_locale, db, ub
from . import gdriveutils as gd
from .constants import STATIC_DIR as _STATIC_DIR
from .subproc_wrapper import process_wait
@ -143,7 +146,7 @@ def check_send_to_kindle_with_converter(formats):
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and not 'MOBI' in formats:
if 'AZW3' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
@ -185,11 +188,11 @@ def check_send_to_kindle(entry):
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
# list with supported formats
def check_read_formats(entry):
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
extensions_reader = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
bookformats = list()
if len(entry.data):
for ele in iter(entry.data):
if ele.format.upper() in EXTENSIONS_READER:
if ele.format.upper() in extensions_reader:
bookformats.append(ele.format.lower())
return bookformats
@ -213,10 +216,10 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
if entry.format.upper() == book_format.upper():
converted_file_name = entry.name + '.' + book_format.lower()
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
EmailText = _(u"%(book)s send to Kindle", book=link)
email_text = _(u"%(book)s send to Kindle", book=link)
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
config.get_mail_settings(), kindle_mail,
EmailText, _(u'This e-mail has been sent via Calibre-Web.')))
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
return
return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -229,15 +232,8 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
if value[-1:] == u'.':
value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0')
if use_unidecode:
if config.config_unicode_filename:
value = (unidecode.unidecode(value))
else:
value = value.replace(u'§', u'SS')
value = value.replace(u'ß', u'ss')
value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
value = re_slugify.sub('', value)
if config.config_unicode_filename:
value = (unidecode.unidecode(value))
if replace_whitespace:
# *+:\"/<>? are replaced by _
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
@ -266,6 +262,7 @@ def split_authors(values):
def get_sorted_author(value):
value2 = None
try:
if ',' not in value:
regexes = [r"^(JR|SR)\.?$", r"^I{1,3}\.?$", r"^IV\.?$"]
@ -290,6 +287,7 @@ def get_sorted_author(value):
value2 = value
return value2
def edit_book_read_status(book_id, read_status=None):
if not config.config_read_column:
book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id),
@ -303,9 +301,9 @@ def edit_book_read_status(book_id, read_status=None):
else:
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status else ub.ReadBook.STATUS_UNREAD
else:
readBook = ub.ReadBook(user_id=current_user.id, book_id = book_id)
readBook.read_status = ub.ReadBook.STATUS_FINISHED
book = readBook
read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id)
read_book.read_status = ub.ReadBook.STATUS_FINISHED
book = read_book
if not book.kobo_reading_state:
kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id)
kobo_reading_state.current_bookmark = ub.KoboBookmark()
@ -332,12 +330,13 @@ def edit_book_read_status(book_id, read_status=None):
except (KeyError, AttributeError):
log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column)
return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column)
except (OperationalError, InvalidRequestError) as e:
except (OperationalError, InvalidRequestError) as ex:
calibre_db.session.rollback()
log.error(u"Read status could not set: {}".format(e))
return _("Read status could not set: {}".format(e.orig))
log.error(u"Read status could not set: {}".format(ex))
return _("Read status could not set: {}".format(ex.orig))
return ""
# Deletes a book fro the local filestorage, returns True if deleting is successfull, otherwise false
def delete_book_file(book, calibrepath, book_format=None):
# check that path is 2 elements deep, check that target path has no subfolders
@ -361,15 +360,15 @@ def delete_book_file(book, calibrepath, book_format=None):
id=book.id,
path=book.path)
shutil.rmtree(path)
except (IOError, OSError) as e:
log.error("Deleting book %s failed: %s", book.id, e)
return False, _("Deleting book %(id)s failed: %(message)s", id=book.id, message=e)
except (IOError, OSError) as ex:
log.error("Deleting book %s failed: %s", book.id, ex)
return False, _("Deleting book %(id)s failed: %(message)s", id=book.id, message=ex)
authorpath = os.path.join(calibrepath, os.path.split(book.path)[0])
if not os.listdir(authorpath):
try:
shutil.rmtree(authorpath)
except (IOError, OSError) as e:
log.error("Deleting authorpath for book %s failed: %s", book.id, e)
except (IOError, OSError) as ex:
log.error("Deleting authorpath for book %s failed: %s", book.id, ex)
return True, None
log.error("Deleting book %s from database only, book path in database not valid: %s",
@ -395,21 +394,21 @@ def clean_author_database(renamed_author, calibre_path="", local_book=None, gdri
all_titledir = book.path.split('/')[1]
all_new_path = os.path.join(calibre_path, all_new_authordir, all_titledir)
all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \
+ get_valid_filename(new_author.name, chars=42)
+ get_valid_filename(new_author.name, chars=42)
# change location in database to new author/title path
book.path = os.path.join(all_new_authordir, all_titledir).replace('\\', '/')
for file_format in book.data:
if not gdrive:
shutil.move(os.path.normcase(os.path.join(all_new_path,
file_format.name + '.' + file_format.format.lower())),
os.path.normcase(os.path.join(all_new_path,
all_new_name + '.' + file_format.format.lower())))
os.path.normcase(os.path.join(all_new_path,
all_new_name + '.' + file_format.format.lower())))
else:
gFile = gd.getFileFromEbooksFolder(all_new_path,
file_format.name + '.' + file_format.format.lower())
if gFile:
gd.moveGdriveFileRemote(gFile, all_new_name + u'.' + file_format.format.lower())
gd.updateDatabaseOnEdit(gFile['id'], all_new_name + u'.' + file_format.format.lower())
g_file = gd.getFileFromEbooksFolder(all_new_path,
file_format.name + '.' + file_format.format.lower())
if g_file:
gd.moveGdriveFileRemote(g_file, all_new_name + u'.' + file_format.format.lower())
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + u'.' + file_format.format.lower())
else:
log.error("File {} not found on gdrive"
.format(all_new_path, file_format.name + '.' + file_format.format.lower()))
@ -426,16 +425,16 @@ def rename_all_authors(first_author, renamed_author, calibre_path="", localbook=
old_author_dir = get_valid_filename(r, chars=96)
new_author_rename_dir = get_valid_filename(new_author.name, chars=96)
if gdrive:
gFile = gd.getFileFromEbooksFolder(None, old_author_dir)
if gFile:
gd.moveGdriveFolderRemote(gFile, new_author_rename_dir)
g_file = gd.getFileFromEbooksFolder(None, old_author_dir)
if g_file:
gd.moveGdriveFolderRemote(g_file, new_author_rename_dir)
else:
if os.path.isdir(os.path.join(calibre_path, old_author_dir)):
try:
old_author_path = os.path.join(calibre_path, old_author_dir)
new_author_path = os.path.join(calibre_path, new_author_rename_dir)
shutil.move(os.path.normcase(old_author_path), os.path.normcase(new_author_path))
except (OSError) as ex:
except OSError as ex:
log.error("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex)
log.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
@ -444,6 +443,7 @@ def rename_all_authors(first_author, renamed_author, calibre_path="", localbook=
new_authordir = get_valid_filename(localbook.authors[0].name, chars=96)
return new_authordir
# Moves files in file storage during author/title rename, or from temp dir to file storage
def update_dir_structure_file(book_id, calibre_path, first_author, original_filepath, db_filename, renamed_author):
# get book database entry from id, if original path overwrite source with original_filepath
@ -483,11 +483,9 @@ def update_dir_structure_file(book_id, calibre_path, first_author, original_file
def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_dir, original_filepath, filename_ext):
error = False
book = calibre_db.get_book(book_id)
file_name = get_valid_filename(title, chars=42) + ' - ' + \
get_valid_filename(first_author, chars=42) + \
filename_ext
get_valid_filename(first_author, chars=42) + filename_ext
rename_all_authors(first_author, renamed_author, gdrive=True)
gdrive_path = os.path.join(get_valid_filename(first_author, chars=96),
title_dir + " (" + str(book_id) + ")")
@ -505,20 +503,20 @@ def update_dir_structure_gdrive(book_id, first_author, renamed_author):
new_titledir = get_valid_filename(book.title, chars=96) + u" (" + str(book_id) + u")"
if titledir != new_titledir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile:
gd.moveGdriveFileRemote(gFile, new_titledir)
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if g_file:
gd.moveGdriveFileRemote(g_file, new_titledir)
book.path = book.path.split('/')[0] + u'/' + new_titledir
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
else:
return _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir and authordir not in renamed_author:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if gFile:
gd.moveGdriveFolderRemote(gFile, new_authordir)
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if g_file:
gd.moveGdriveFolderRemote(g_file, new_authordir)
book.path = new_authordir + u'/' + book.path.split('/')[1]
gd.updateDatabaseOnEdit(gFile['id'], book.path)
gd.updateDatabaseOnEdit(g_file['id'], book.path)
else:
return _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
@ -542,15 +540,15 @@ def move_files_on_change(calibre_path, new_authordir, new_titledir, localbook, d
# move original path to new path
log.debug("Moving title: %s to %s", path, new_path)
shutil.move(os.path.normcase(path), os.path.normcase(new_path))
else: # path is valid copy only files to new location (merge)
else: # path is valid copy only files to new location (merge)
log.info("Moving title: %s into existing: %s", path, new_path)
# Take all files and subfolder from old path (strange command)
for dir_name, __, file_list in os.walk(path):
for file in file_list:
shutil.move(os.path.normcase(os.path.join(dir_name, file)),
os.path.normcase(os.path.join(new_path + dir_name[len(path):], file)))
os.path.normcase(os.path.join(new_path + dir_name[len(path):], file)))
# change location in database to new author/title path
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\','/')
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
except OSError as ex:
log.error("Rename title from: %s to %s: %s", path, new_path, ex)
log.debug(ex, exc_info=True)
@ -587,12 +585,12 @@ def delete_book_gdrive(book, book_format):
for entry in book.data:
if entry.format.upper() == book_format:
name = entry.name + '.' + book_format
gFile = gd.getFileFromEbooksFolder(book.path, name)
g_file = gd.getFileFromEbooksFolder(book.path, name)
else:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), book.path.split('/')[1])
if gFile:
gd.deleteDatabaseEntry(gFile['id'])
gFile.Trash()
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), book.path.split('/')[1])
if g_file:
gd.deleteDatabaseEntry(g_file['id'])
g_file.Trash()
else:
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
@ -624,12 +622,13 @@ def generate_random_password():
def uniq(inpt):
output = []
inpt = [ " ".join(inp.split()) for inp in inpt]
inpt = [" ".join(inp.split()) for inp in inpt]
for x in inpt:
if x not in output:
output.append(x)
return output
def check_email(email):
email = valid_email(email)
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
@ -642,7 +641,7 @@ def check_username(username):
username = username.strip()
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
log.error(u"This username is already taken")
raise Exception (_(u"This username is already taken"))
raise Exception(_(u"This username is already taken"))
return username
@ -728,13 +727,13 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
# saves book cover from url
def save_cover_from_url(url, book_path):
try:
if not cli.allow_localhost:
# 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1" or ip == "0.0.0.0" or ip == "::":
log.error("Localhost was accessed for cover upload")
return False, _("You are not allowed to access localhost for cover uploads")
img = requests.get(url, timeout=(10, 200), allow_redirects=False) # ToDo: Error Handling
if cli.allow_localhost:
img = requests.get(url, timeout=(10, 200), allow_redirects=False) # ToDo: Error Handling
elif use_advocate:
img = advocate.get(url, timeout=(10, 200), allow_redirects=False) # ToDo: Error Handling
else:
log.error("python modul advocate is not installed but is needed")
return False, _("Python modul 'advocate' is not installed but is needed for cover downloads")
img.raise_for_status()
return save_cover(img, book_path)
except (socket.gaierror,
@ -746,6 +745,9 @@ def save_cover_from_url(url, book_path):
except MissingDelegateError as ex:
log.info(u'File Format Error %s', ex)
return False, _("Cover Format Error")
except UnacceptableAddressException:
log.error("Localhost was accessed for cover upload")
return False, _("You are not allowed to access localhost for cover uploads")
def save_cover_from_filestorage(filepath, saved_filename, img):
@ -808,7 +810,7 @@ def save_cover(img, book_path):
os.mkdir(tmp_dir)
ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img)
if ret is True:
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"),
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\", "/"),
os.path.join(tmp_dir, "uploaded_cover.jpg"))
log.info("Cover is saved on Google Drive")
return True, None
@ -820,9 +822,9 @@ def save_cover(img, book_path):
def do_download_file(book, book_format, client, data, headers):
if config.config_use_google_drive:
#startTime = time.time()
# startTime = time.time()
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
#log.debug('%s', time.time() - startTime)
# log.debug('%s', time.time() - startTime)
if df:
return gd.do_gdrive_download(df, headers)
else:
@ -846,16 +848,16 @@ def do_download_file(book, book_format, client, data, headers):
##################################
def check_unrar(unrarLocation):
if not unrarLocation:
def check_unrar(unrar_location):
if not unrar_location:
return
if not os.path.exists(unrarLocation):
if not os.path.exists(unrar_location):
return _('Unrar binary file not found')
try:
unrarLocation = [unrarLocation]
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
unrar_location = [unrar_location]
value = process_wait(unrar_location, pattern='UNRAR (.*) freeware')
if value:
version = value.group(1)
log.debug("unrar version %s", version)
@ -882,19 +884,19 @@ def json_serial(obj):
# helper function for displaying the runtime of tasks
def format_runtime(runtime):
retVal = ""
ret_val = ""
if runtime.days:
retVal = format_unit(runtime.days, 'duration-day', length="long", locale=get_locale()) + ', '
ret_val = format_unit(runtime.days, 'duration-day', length="long", locale=get_locale()) + ', '
mins, seconds = divmod(runtime.seconds, 60)
hours, minutes = divmod(mins, 60)
# ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ?
if hours:
retVal += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds)
ret_val += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds)
elif minutes:
retVal += '{:2d}:{:02d}s'.format(minutes, seconds)
ret_val += '{:2d}:{:02d}s'.format(minutes, seconds)
else:
retVal += '{:2d}s'.format(seconds)
return retVal
ret_val += '{:2d}s'.format(seconds)
return ret_val
# helper function to apply localize status information in tasklist entries
@ -951,8 +953,8 @@ def check_valid_domain(domain_text):
def get_cc_columns(filter_config_custom_read=False):
tmpcc = calibre_db.session.query(db.Custom_Columns)\
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
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:
@ -971,6 +973,7 @@ def get_cc_columns(filter_config_custom_read=False):
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)
data1= ""
if book:
data1 = calibre_db.get_book_format(book.id, book_format.upper())
else:

@ -28,7 +28,6 @@ from flask import Blueprint, request, render_template, Response, g, make_respons
from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_, true
from werkzeug.security import check_password_hash
from tornado.httputil import HTTPServerRequest
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
@ -99,26 +98,7 @@ def feed_normal_search():
@opds.route("/opds/books")
@requires_basic_auth_if_no_ano
def feed_booksindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\
.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_books',
pagination=pagination)
return render_element_index(db.Books.sort, None, 'opds.feed_letter_books')
@opds.route("/opds/books/letter/<book_id>")
@ -171,43 +151,23 @@ def feed_hot():
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
downloadBook = calibre_db.get_book(book.Downloads.book_id)
if downloadBook:
download_book = calibre_db.get_book(book.Downloads.book_id)
if download_book:
entries.append(
calibre_db.get_filtered_book(book.Downloads.book_id)
)
else:
ub.delete_download(book.Downloads.book_id)
numBooks = entries.__len__()
num_books = entries.__len__()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
config.config_books_per_page, numBooks)
config.config_books_per_page, num_books)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/author")
@requires_basic_auth_if_no_ano
def feed_authorindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_author',
pagination=pagination)
return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
@opds.route("/opds/author/letter/<book_id>")
@ -228,12 +188,7 @@ def feed_letter_author(book_id):
@opds.route("/opds/author/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_author(book_id):
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.authors.any(db.Authors.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
return render_xml_dataset(db.Authors, book_id)
@opds.route("/opds/publisher")
@ -254,37 +209,14 @@ def feed_publisherindex():
@opds.route("/opds/publisher/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_publisher(book_id):
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.publishers.any(db.Publishers.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
return render_xml_dataset(db.Publishers, book_id)
@opds.route("/opds/category")
@requires_basic_auth_if_no_ano
def feed_categoryindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_category',
pagination=pagination)
@opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano
@ -306,36 +238,14 @@ def feed_letter_category(book_id):
@opds.route("/opds/category/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_category(book_id):
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.tags.any(db.Tags.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
return render_xml_dataset(db.Tags, book_id)
@opds.route("/opds/series")
@requires_basic_auth_if_no_ano
def feed_seriesindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_series',
pagination=pagination)
return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
@opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano
@ -370,7 +280,7 @@ def feed_series(book_id):
def feed_ratingindex():
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
@ -388,12 +298,7 @@ def feed_ratingindex():
@opds.route("/opds/ratings/<book_id>")
@requires_basic_auth_if_no_ano
def feed_ratings(book_id):
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.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
return render_xml_dataset(db.Tags, book_id)
@opds.route("/opds/formats")
@ -491,7 +396,7 @@ def feed_shelf(book_id):
@requires_basic_auth_if_no_ano
def opds_download_link(book_id, book_format):
# I gave up with this: With enabled ldap login, the user doesn't get logged in, therefore it's always guest
# workaround, loading the user from the request and checking it's download rights here
# workaround, loading the user from the request and checking its download rights here
# in case of anonymous browsing user is None
user = load_user_from_request(request) or current_user
if not user.role_download():
@ -517,6 +422,31 @@ def get_metadata_calibre_companion(uuid, library):
return ""
@opds.route("/opds/thumb_240_240/<book_id>")
@opds.route("/opds/cover_240_240/<book_id>")
@opds.route("/opds/cover_90_90/<book_id>")
@opds.route("/opds/cover/<book_id>")
@requires_basic_auth_if_no_ano
def feed_get_cover(book_id):
return get_book_cover(book_id)
@opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano
def feed_read_books():
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@opds.route("/opds/unreadbooks")
@requires_basic_auth_if_no_ano
def feed_unread_books():
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
def feed_search(term):
if term:
entries, __, ___ = calibre_db.get_search_results(term, config_read_column=config.config_read_column)
@ -538,8 +468,8 @@ def check_auth(username, password):
if bool(user and check_password_hash(str(user.password), password)):
return True
else:
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address)
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_address)
return False
@ -559,26 +489,33 @@ def render_xml_template(*args, **kwargs):
return response
@opds.route("/opds/thumb_240_240/<book_id>")
@opds.route("/opds/cover_240_240/<book_id>")
@opds.route("/opds/cover_90_90/<book_id>")
@opds.route("/opds/cover/<book_id>")
@requires_basic_auth_if_no_ano
def feed_get_cover(book_id):
return get_book_cover(book_id)
@opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano
def feed_read_books():
def render_xml_dataset(data_table, book_id):
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
data_table.any(data_table.id == book_id),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/unreadbooks")
@requires_basic_auth_if_no_ano
def feed_unread_books():
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
return render_xml_template('feed.xml', entries=result, 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'))
if linked_table:
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()
elements = []
if off == 0:
elements.append({'id': "00", 'name': _("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder=folder,
pagination=pagination)

@ -57,10 +57,10 @@ class Pagination(object):
def has_next(self):
return self.page < self.pages
# right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shwn
# left_edge: first left_edges count of all pages are shown as number -> 1,2 shwn
# left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shwn
# left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shwn
# right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shown
# left_edge: first left_edges count of all pages are shown as number -> 1,2 shown
# left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shown
# left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shown
def iter_pages(self, left_edge=2, left_current=2,
right_current=4, right_edge=2):
last = 0

@ -22,6 +22,7 @@
import json
from datetime import datetime
from functools import wraps
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
from flask_login import login_required, current_user, login_user
@ -31,10 +32,6 @@ from sqlalchemy.sql.expression import true
from . import config, logger, ub
from .render_template import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
remotelogin = Blueprint('remotelogin', __name__)
log = logger.create()

@ -22,7 +22,7 @@
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
<form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<form class="padded-bottom" action="{{ url_for('edit-book.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<div class="text-left">
@ -48,7 +48,7 @@
{% endif %}
</div>
<form role="form" action="{{ url_for('editbook.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
<form role="form" action="{{ url_for('edit-book.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-sm-9 col-xs-12">
<div class="form-group">

@ -6,7 +6,7 @@
data-escape="true"
{% if g.user.role_edit() %}
data-editable-type="text"
data-editable-url="{{ url_for('editbook.edit_list_book', param=parameter)}}"
data-editable-url="{{ url_for('edit-book.edit_list_book', param=parameter)}}"
data-editable-title="{{ edit_text }}"
data-edit="true"
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}" {% endif %}
@ -66,30 +66,30 @@
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }}
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter Title')}}"{% endif %}>{{_('Series Index')}}</th>
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('edit-book.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter Title')}}"{% endif %}>{{_('Series Index')}}</th>
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }}
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }}
<th data-field="comments" id="comments" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('comments')}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('editbook.edit_list_book', param='comments')}}" data-edit="true" data-editable-title="{{_('Enter comments')}}"{% endif %}>{{_('Comments')}}</th>
<th data-field="comments" id="comments" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('comments')}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='comments')}}" data-edit="true" data-editable-title="{{_('Enter comments')}}"{% endif %}>{{_('Comments')}}</th>
{% if g.user.check_visibility(32768) %}
{{ book_checkbox_row('is_archived', _('Archiv Status'), false)}}
{% endif %}
{{ book_checkbox_row('read_status', _('Read Status'), false)}}
{% for c in cc %}
{% if c.datatype == "int" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="1" data-editable-url="{{ url_for('editbook.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="1" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "rating" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-formatter="ratingFormatter" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.5" data-editable-step="1" data-editable-min="1" data-editable-max="5" data-editable-url="{{ url_for('editbook.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-formatter="ratingFormatter" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.5" data-editable-step="1" data-editable-min="1" data-editable-max="5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "float" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-url="{{ url_for('editbook.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "enumeration" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="select" data-editable-source={{ url_for('editbook.table_get_custom_enum', c_id=c.id) }} data-editable-url="{{ url_for('editbook.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="select" data-editable-source={{ url_for('edit-book.table_get_custom_enum', c_id=c.id) }} data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype in ["datetime"] %}
<!-- missing -->
{% elif c.datatype == "text" %}
{{ text_table_row('custom_column_' + c.id|string, _('Enter ') + c.name, c.name, false, false) }}
{% elif c.datatype == "comments" %}
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('editbook.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
<th data-field="custom_column_{{ c.id|string }}" id="custom_column_{{ c.id|string }}" data-escape="true" data-editable-mode="popup" data-visible="{{visiblility.get('custom_column_'+ c.id|string)}}" data-sortable="false" {% if g.user.role_edit() %} data-editable-type="wysihtml5" data-editable-url="{{ url_for('edit-book.edit_list_book', param='custom_column_'+ c.id|string)}}" data-edit="true" data-editable-title="{{_('Enter ') + c.name}}"{% endif %}>{{c.name}}</th>
{% elif c.datatype == "bool" %}
{{ book_checkbox_row('custom_column_' + c.id|string, c.name, false)}}
{% else %}

@ -138,7 +138,7 @@
<p>
<span class="glyphicon glyphicon-link"></span>
{% for identifier in entry.identifiers %}
<a href="{{identifier}}" target="_blank" class="btn btn-xs btn-success" role="button">{{identifier.formatType()}}</a>
<a href="{{identifier}}" target="_blank" class="btn btn-xs btn-success" role="button">{{identifier.format_type()}}</a>
{%endfor%}
</p>
</div>
@ -295,7 +295,7 @@
{% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('editbook.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
<a href="{{ url_for('edit-book.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
</div>
</div>
{% endif %}

@ -60,7 +60,7 @@
{% if g.user.is_authenticated or g.allow_anonymous %}
{% if g.user.role_upload() and g.allow_upload %}
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<form id="form-upload" class="navbar-form" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"

@ -18,6 +18,7 @@
import base64
import binascii
from functools import wraps
from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash
@ -25,10 +26,6 @@ from flask_login import login_required, login_user
from . import lm, ub, config, constants, services
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
def login_required_if_no_ano(func):
@wraps(func)

@ -29,7 +29,7 @@ import copy
from functools import wraps
from babel.dates import format_date
from babel import Locale as LC
from babel import Locale
from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session
@ -60,7 +60,6 @@ from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template
from .kobo_sync_status import change_archived_books
feature_support = {
'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support),
@ -69,10 +68,12 @@ feature_support = {
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
try:
from natsort import natsorted as sort
@ -82,8 +83,11 @@ except ImportError:
@app.after_request
def add_security_headers(resp):
resp.headers['Content-Security-Policy'] = "default-src 'self'" + ''.join([' '+host for host in config.config_trustedhosts.strip().split(',')]) + " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:"
if request.endpoint == "editbook.edit_book" or config.config_use_google_drive:
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:"
resp.headers['Content-Security-Policy'] = csp
if request.endpoint == "edit-book.edit_book" or config.config_use_google_drive:
resp.headers['Content-Security-Policy'] += " *"
elif request.endpoint == "web.read_book":
resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';"
@ -93,6 +97,7 @@ def add_security_headers(resp):
resp.headers['Strict-Transport-Security'] = 'max-age=31536000;'
return resp
web = Blueprint('web', __name__)
log = logger.create()
@ -119,6 +124,7 @@ def viewer_required(f):
return inner
# ################################### data provider functions #########################################################
@ -140,11 +146,11 @@ def set_bookmark(book_id, book_format):
ub.session_commit()
return "", 204
lbookmark = ub.Bookmark(user_id=current_user.id,
book_id=book_id,
format=book_format,
bookmark_key=bookmark_key)
ub.session.merge(lbookmark)
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
@ -162,7 +168,7 @@ def toggle_read(book_id):
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
@login_required
def toggle_archived(book_id):
is_archived = change_archived_books(book_id, message="Book {} archivebit toggled".format(book_id))
is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
if is_archived:
remove_synced_book(book_id)
return ""
@ -230,6 +236,7 @@ def get_comic_book(book_id, book_format, page):
return "", 204
'''
# ################################### Typeahead ##################################################################
@ -297,6 +304,12 @@ def get_matching_tags():
return json_dumps
def generate_char_list(data_colum, db_link):
return (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())
def get_sort_function(sort_param, data):
order = [db.Books.timestamp.desc()]
if sort_param == 'stored':
@ -373,7 +386,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,
False, 0,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@ -407,12 +420,13 @@ def render_discover_books(page, book_id):
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 comparsion 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())):
# Unary expression comparsion 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 = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
@ -420,19 +434,19 @@ def render_hot_books(page, order):
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))\
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:
downloadBook = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).filter(
download_book = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).filter(
db.Books.id == book.Downloads.book_id).first()
if downloadBook:
entries.append(downloadBook)
if download_book:
entries.append(download_book)
else:
ub.delete_download(book.Downloads.book_id)
numBooks = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks)
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=_(u"Hot Books (Most Downloaded)"), page="hot", order=order[1])
else:
@ -462,8 +476,8 @@ def render_downloaded_books(page, order, user_id):
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():
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)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html',
@ -471,7 +485,7 @@ def render_downloaded_books(page, order, user_id):
entries=entries,
pagination=pagination,
id=user_id,
title=_(u"Downloaded books by %(user)s",user=user.name),
title=_(u"Downloaded books by %(user)s", user=user.name),
page="download",
order=order[1])
else:
@ -639,29 +653,27 @@ def render_read_books(page, are_read, as_xml=False, order=None):
column=config.config_read_column),
category="error")
return redirect(url_for("web.index"))
return [] # ToDo: Handle error Case for opds
return [] # ToDo: Handle error Case for opds
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read"
page_name = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread"
page_name = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename, order=order[1])
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_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)
@ -674,40 +686,40 @@ def render_archived_books(page, sort_param):
False, 0)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived"
page_name = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename, order=sort_param[1])
title=name, page=page_name, order=sort_param[1])
def render_prepare_search_form(cc):
# prepare data for search-form
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
tags = calibre_db.session.query(db.Tags) \
.join(db.books_tags_link) \
.join(db.Books) \
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.group_by(text('books_tags_link.tag')) \
.order_by(db.Tags.name).all()
series = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
series = calibre_db.session.query(db.Series) \
.join(db.books_series_link) \
.join(db.Books) \
.filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series'))\
.order_by(db.Series.name)\
.group_by(text('books_series_link.series')) \
.order_by(db.Series.name) \
.filter(calibre_db.common_filters()).all()
shelves = ub.session.query(ub.Shelf)\
.filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\
shelves = ub.session.query(ub.Shelf) \
.filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id))) \
.order_by(ub.Shelf.name).all()
extensions = calibre_db.session.query(db.Data)\
.join(db.Books)\
extensions = calibre_db.session.query(db.Data) \
.join(db.Books) \
.filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\
.group_by(db.Data.format) \
.order_by(db.Data.format).all()
if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language()
else:
languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
series=series, shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None):
@ -716,7 +728,6 @@ def render_search_results(term, offset=None, order=None, limit=None):
offset,
order,
limit,
False,
config.config_read_column,
*join)
return render_title_template('search.html',
@ -759,12 +770,13 @@ def books_table():
return render_title_template('book_table.html', title=_(u"Books List"), cc=cc, page="book_table",
visiblility=visibility)
@web.route("/ajax/listbooks")
@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 = request.args.get("search")
search_param = request.args.get("search")
sort_param = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
@ -784,8 +796,8 @@ def list_books():
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
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
@ -794,10 +806,11 @@ def list_books():
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()
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:
books = calibre_db.search_query(search, config.config_read_column).all()
if search_param:
books = calibre_db.search_query(search_param, config.config_read_column).all()
filtered_count = len(books)
else:
if not config.config_read_column:
@ -818,15 +831,14 @@ def list_books():
# 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))
int(current_user.id) == ub.ArchivedBook.user_id))
.filter(calibre_db.common_filters(allow_show_archived=True)).all())
entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order, True)
elif search:
entries, filtered_count, __ = calibre_db.get_search_results(search,
elif search_param:
entries, filtered_count, __ = calibre_db.get_search_results(search_param,
off,
[order,''],
[order, ''],
limit,
True,
config.config_read_column,
*join)
else:
@ -845,9 +857,9 @@ def list_books():
val = entry[0]
val.read_status = entry[1] == ub.ReadBook.STATUS_FINISHED
val.is_archived = entry[2] is True
for index in range(0, len(val.languages)):
val.languages[index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[
index].lang_code)
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}
@ -857,6 +869,7 @@ def list_books():
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@web.route("/ajax/table_settings", methods=['POST'])
@login_required
def update_table_settings():
@ -886,19 +899,18 @@ def author_list():
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()
charlist = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('char')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
char_list = generate_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
autor_copy = copy.deepcopy(entries)
for entry in autor_copy:
author_copy = copy.deepcopy(entries)
for entry in author_copy:
entry.Authors.name = entry.Authors.name.replace('|', ',')
return render_title_template('list.html', entries=autor_copy, folder='web.books_list', charlist=charlist,
return render_title_template('list.html', entries=author_copy, folder='web.books_list', charlist=char_list,
title=u"Authors", page="authorlist", data='author', order=order_no)
else:
abort(404)
@web.route("/downloadlist")
@login_required_if_no_ano
def download_list():
@ -909,12 +921,12 @@ def download_list():
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'))\
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()
charlist = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
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=charlist,
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Downloads"), page="downloadlist", data="download", order=order_no)
else:
abort(404)
@ -933,10 +945,8 @@ def publisher_list():
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()
charlist = calibre_db.session.query(func.upper(func.substr(db.Publishers.name, 1, 1)).label('char')) \
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
char_list = generate_char_list(db.Publishers.name, db.books_publishers_link)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
else:
abort(404)
@ -952,25 +962,19 @@ def series_list():
else:
order = db.Series.sort.asc()
order_no = 1
char_list = generate_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()
charlist = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"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())\
.join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(order).all()
charlist = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no)
else:
@ -988,7 +992,7 @@ def ratings_list():
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')) \
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
@ -1023,14 +1027,14 @@ def formats_list():
def language_overview():
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == u"all":
order_no = 0 if current_user.get_view_property('language', 'dir') == 'desc' else 1
charlist = list()
char_list = list()
languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
for lang in languages:
upper_lang = lang[0].name[0].upper()
if upper_lang not in charlist:
charlist.append(upper_lang)
if upper_lang not in char_list:
char_list.append(upper_lang)
return render_title_template('languages.html', languages=languages,
charlist=charlist, title=_(u"Languages"), page="langlist",
charlist=char_list, title=_(u"Languages"), page="langlist",
data="language", order=order_no)
else:
abort(404)
@ -1049,10 +1053,8 @@ def category_list():
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(text('books_tags_link.tag')).all()
charlist = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('char')) \
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
char_list = generate_char_list(db.Tags.name, db.books_tags_link)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Categories"), page="catlist", data="category", order=order_no)
else:
abort(404)
@ -1176,7 +1178,15 @@ def adv_search_read_status(q, read_status):
return q
def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
def adv_search_text(q, include_inputs, exclude_inputs, data_value):
for inp in include_inputs:
q = q.filter(db.Books.data.any(data_value == inp))
for excl in exclude_inputs:
q = q.filter(not_(db.Books.data.any(data_value == excl)))
return q
'''def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
for extension in include_extension_inputs:
q = q.filter(db.Books.data.any(db.Data.format == extension))
for extension in exclude_extension_inputs:
@ -1197,15 +1207,17 @@ def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
q = q.filter(db.Books.series.any(db.Series.id == serie))
for serie in exclude_series_inputs:
q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
return q
return q'''
def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\
q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id) \
.filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
if len(include_shelf_inputs) > 0:
q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
return q
def extend_search_term(searchterm,
author_name,
book_title,
@ -1232,7 +1244,7 @@ def extend_search_term(searchterm,
format='medium', locale=get_locale())])
except ValueError:
pub_end = u""
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
elements = {'tag': db.Tags, 'serie': db.Series, 'shelf': ub.Shelf}
for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
@ -1284,8 +1296,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
query = query.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
int(current_user.id) == ub.ArchivedBook.user_id))
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series)\
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book) \
.outerjoin(db.Series) \
.filter(calibre_db.common_filters(True))
# parse multiselects to a complete dict
@ -1311,43 +1323,43 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
if publisher:
publisher = publisher.strip().lower()
searchterm = []
search_term = []
cc_present = False
for c in cc:
if c.datatype == "datetime":
column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start:
searchterm.extend([u"{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium',
locale=get_locale())
)])
search_term.extend([u"{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium',
locale=get_locale())
)])
cc_present = True
if column_end:
searchterm.extend([u"{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium',
locale=get_locale())
)])
search_term.extend([u"{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium',
locale=get_locale())
)])
cc_present = True
elif term.get('custom_column_' + str(c.id)):
searchterm.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
or rating_high or description or cc_present or read_status:
searchterm, pub_start, pub_end = extend_search_term(searchterm,
author_name,
book_title,
publisher,
pub_start,
pub_end,
tags,
rating_high,
rating_low,
read_status)
if any(tags.values()) or author_name or book_title or \
publisher or pub_start or pub_end or rating_low or rating_high \
or description or cc_present or read_status:
search_term, pub_start, pub_end = extend_search_term(search_term,
author_name,
book_title,
publisher,
pub_start,
pub_end,
tags,
rating_high,
rating_low,
read_status)
# q = q.filter()
if author_name:
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_name + "%")))
@ -1360,12 +1372,12 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
q = adv_search_read_status(q, read_status)
if publisher:
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
q = adv_search_tag(q, tags['include_tag'], tags['exclude_tag'])
q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie'])
q = adv_search_text(q, tags['include_tag'], tags['exclude_tag'], db.Tags.id)
q = adv_search_text(q, tags['include_serie'], tags['exclude_serie'], db.Series.id)
q = adv_search_text(q, tags['include_extension'], tags['exclude_extension'], db.Data.format)
q = adv_search_shelf(q, tags['include_shelf'], tags['exclude_shelf'])
q = adv_search_extension(q, tags['include_extension'], tags['exclude_extension'])
q = adv_search_language(q, tags['include_language'], tags['exclude_language'])
q = adv_search_ratings(q, rating_high, rating_low)
q = adv_search_language(q, tags['include_language'], tags['exclude_language'], )
q = adv_search_ratings(q, rating_high, rating_low, )
if description:
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
@ -1390,7 +1402,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
limit_all = result_count
entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
return render_title_template('search.html',
adv_searchterm=searchterm,
adv_searchterm=search_term,
pagination=pagination,
entries=entries,
result_count=result_count,
@ -1414,10 +1426,12 @@ def advanced_search_form():
def get_cover(book_id):
return get_book_cover(book_id)
@web.route("/robots.txt")
def get_robots():
return send_from_directory(constants.STATIC_DIR, "robots.txt")
@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
@ -1561,7 +1575,7 @@ def login():
category="success")
return redirect_back(url_for("web.index"))
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
and user.name != "Guest":
and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
log.info("Local Fallback Login as: '%s'", user.name)
@ -1573,23 +1587,23 @@ def login():
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', form['username'], ip_Address)
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address)
flash(_(u"Wrong Username or Password"), category="error")
else:
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
if 'forgot' in form and form['forgot'] == 'forgot':
if user is not None and user.name != "Guest":
ret, __ = reset_password(user.id)
if ret == 1:
flash(_(u"New Password was send to your email address"), category="info")
log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_Address)
log.info('Password reset for user "%s" IP-address: %s', form['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)
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":
login_user(user, remember=bool(form.get('remember_me')))
@ -1599,7 +1613,7 @@ def login():
config.config_is_initial = False
return redirect_back(url_for("web.index"))
else:
log.warning('Login failed for user "%s" IP-address: %s', form['username'], ip_Address)
log.warning('Login failed for user "%s" IP-address: %s', form['username'], ip_address)
flash(_(u"Wrong Username or Password"), category="error")
next_url = request.args.get('next', default=url_for("web.index"), type=str)
@ -1617,7 +1631,7 @@ def login():
@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',""))
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()
@ -1639,7 +1653,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
current_user.email = check_email(to_save["email"])
if current_user.role_admin():
if to_save.get("name", current_user.name) != current_user.name:
# Query User name, if not existing, change
# Query username, if not existing, change
current_user.name = check_username(to_save["name"])
current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
if to_save.get("default_language"):
@ -1693,7 +1707,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
@login_required
def profile():
languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')]
translations = babel.list_translations() + [Locale('en')]
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()
@ -1727,7 +1741,8 @@ def read_book(book_id, book_format):
book.ordered_authors = calibre_db.order_authors([book], False)
if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error")
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
return redirect(url_for("web.index"))
@ -1768,7 +1783,8 @@ def read_book(book_id, book_format):
return render_title_template('readcbr.html', comicfile=all_name, title=title,
extension=fileExt)
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error")
return redirect(url_for("web.index"))
@ -1782,14 +1798,14 @@ def show_book(book_id):
entry = entries[0]
entry.read_status = read_book == ub.ReadBook.STATUS_FINISHED
entry.is_archived = archived_book
for index in range(0, len(entry.languages)):
entry.languages[index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
index].lang_code)
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)
book_in_shelfs = []
book_in_shelves = []
shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
for sh in shelfs:
book_in_shelfs.append(sh.shelf)
book_in_shelves.append(sh.shelf)
entry.tags = sort(entry.tags, key=lambda tag: tag.name)
@ -1806,9 +1822,9 @@ def show_book(book_id):
return render_title_template('detail.html',
entry=entry,
cc=cc,
is_xhr=request.headers.get('X-Requested-With')=='XMLHttpRequest',
is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
title=entry.title,
books_shelfs=book_in_shelfs,
books_shelfs=book_in_shelves,
page="book")
else:
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")

@ -1,5 +1,5 @@
# GDrive Integration
google-api-python-client>=1.7.11,<2.37.0
google-api-python-client>=1.7.11,<2.41.0
gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0
@ -12,8 +12,8 @@ PyYAML>=3.12
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-auth-oauthlib>=0.4.3,<0.6.0
google-api-python-client>=1.7.11,<2.41.0
# goodreads
goodreads>=0.3.2,<0.4.0
@ -29,7 +29,7 @@ SQLAlchemy-Utils>=0.33.5,<0.39.0
# metadata extraction
rarfile>=3.2
scholarly>=1.2.0,<1.6
scholarly>=1.2.0,<1.7
markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1
python-dateutil>=2.1,<2.9.0

@ -12,6 +12,7 @@ SQLAlchemy>=1.3.0,<1.5.0
tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<4.8.0
lxml>=3.8.0,<4.9.0
flask-wtf>=0.14.2,<1.1.0
chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0

Loading…
Cancel
Save