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

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

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

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

@ -31,7 +31,7 @@ from functools import wraps
try: try:
from lxml.html.clean import clean_html from lxml.html.clean import clean_html
except ImportError: except ImportError:
pass clean_html = None
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ 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 from .kobo_sync_status import change_archived_books
editbook = Blueprint('editbook', __name__) EditBook = Blueprint('edit-book', __name__)
log = logger.create() log = logger.create()
@ -61,6 +61,7 @@ def upload_required(f):
return inner return inner
def edit_required(f): def edit_required(f):
@wraps(f) @wraps(f)
def inner(*args, **kwargs): def inner(*args, **kwargs):
@ -70,6 +71,7 @@ def edit_required(f):
return inner return inner
def search_objects_remove(db_book_object, db_type, input_elements): def search_objects_remove(db_book_object, db_type, input_elements):
del_elements = [] del_elements = []
for c_elements in db_book_object: 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) db_session.delete(del_element)
return changed return changed
def add_objects(db_book_object, db_object, db_session, db_type, add_elements): def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
changed = False changed = False
if db_type == 'languages': if db_type == 'languages':
@ -128,7 +131,7 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
else: else:
db_filter = db_object.name db_filter = db_object.name
for add_element in add_elements: 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() db_element = db_session.query(db_object).filter(db_filter == add_element).first()
# if no element is found add it # if no element is found add it
if db_type == 'author': 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) db_book_object.append(new_element)
else: else:
db_element = create_objects_for_addition(db_element, add_element, db_type) db_element = create_objects_for_addition(db_element, add_element, db_type)
changed = True
# add element to book # add element to book
changed = True changed = True
db_book_object.append(db_element) db_book_object.append(db_element)
@ -178,7 +180,7 @@ def create_objects_for_addition(db_element, add_element, db_type):
return db_element 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 # 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): 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 # 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]) input_dict = dict([(identifier.type.lower(), identifier) for identifier in input_identifiers])
if len(input_identifiers) != len(input_dict): if len(input_identifiers) != len(input_dict):
error = True 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 # delete db identifiers not present in input or modify them with input val
for identifier_type, identifier in db_dict.items(): for identifier_type, identifier in db_dict.items():
if identifier_type not in input_dict.keys(): if identifier_type not in input_dict.keys():
@ -224,14 +226,15 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True changed = True
return changed, error return changed, error
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@EditBook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@login_required @login_required
def delete_book_from_details(book_id): def delete_book_from_details(book_id):
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json') 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>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"]) @EditBook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@login_required @login_required
def delete_book_ajax(book_id, book_format): def delete_book_ajax(book_id, book_format):
return delete_book_from_table(book_id, book_format, False) 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.languages, db.Languages, calibre_db.session, 'languages')
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers') modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
cc = calibre_db.session.query(db.Custom_Columns). \ cc = calibre_db.session.query(db.CustomColumns). \
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
for c in cc: for c in cc:
cc_string = "custom_column_" + str(c.id) cc_string = "custom_column_" + str(c.id)
if not c.is_multiple: 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() 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 book_format:
if jsonResponse: if json_response:
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id), return json.dumps([warning, {"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "success", "type": "success",
"format": book_format, "format": book_format,
"message": _('Book Format Successfully Deleted')}]) "message": _('Book Format Successfully Deleted')}])
else: else:
flash(_('Book Format Successfully Deleted'), category="success") 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: else:
if jsonResponse: if json_response:
return json.dumps([warning, {"location": url_for('web.index'), return json.dumps([warning, {"location": url_for('web.index'),
"type": "success", "type": "success",
"format": book_format, "format": book_format,
@ -304,7 +307,7 @@ def render_delete_book_result(book_format, jsonResponse, warning, book_id):
return redirect(url_for('web.index')) 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 = {} warning = {}
if current_user.role_delete_books(): if current_user.role_delete_books():
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
@ -312,20 +315,20 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
try: try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result: if not result:
if jsonResponse: if json_response:
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": error}]) "message": error}])
else: else:
flash(error, category="error") 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 error:
if jsonResponse: if json_response:
warning = {"location": url_for("editbook.edit_book", book_id=book_id), warning = {"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "warning", "type": "warning",
"format": "", "format": "",
"message": error} "message": error}
else: else:
flash(error, category="warning") flash(error, category="warning")
if not book_format: if not book_format:
@ -339,35 +342,36 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
except Exception as ex: except Exception as ex:
log.error_or_exception(ex) log.error_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
if jsonResponse: if json_response:
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": ex}]) "message": ex}])
else: else:
flash(str(ex), category="error") 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: else:
# book not found # book not found
log.error('Book with id "%s" could not be deleted: not found', book_id) 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") message = _("You are missing permissions to delete books")
if jsonResponse: if json_response:
return json.dumps({"location": url_for("editbook.edit_book", book_id=book_id), return json.dumps({"location": url_for("edit-book.edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": message}) "message": message})
else: else:
flash(message, category="error") 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): 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) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
if not book: 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")) return redirect(url_for("web.index"))
for lang in book.languages: for lang in book.languages:
@ -380,9 +384,9 @@ def render_edit_book(book_id):
author_names.append(authr.name.replace('|', ',')) author_names.append(authr.name.replace('|', ','))
# Option for showing convertbook button # Option for showing convertbook button
valid_source_formats=list() valid_source_formats = list()
allowed_conversion_formats = list() allowed_conversion_formats = list()
kepub_possible=None kepub_possible = None
if config.config_converterpath: if config.config_converterpath:
for file in book.data: for file in book.data:
if file.format.lower() in constants.EXTENSIONS_CONVERT_FROM: if file.format.lower() in constants.EXTENSIONS_CONVERT_FROM:
@ -430,6 +434,7 @@ def edit_book_ratings(to_save, book):
changed = True changed = True
return changed return changed
def edit_book_tags(tags, book): def edit_book_tags(tags, book):
input_tags = tags.split(',') input_tags = tags.split(',')
input_tags = list(map(lambda it: it.strip(), input_tags)) 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): def edit_book_series_index(series_index, book):
# Add default series_index to book # Add default series_index to book
modif_date = False modify_date = False
series_index = series_index or '1' series_index = series_index or '1'
if not series_index.replace('.', '', 1).isdigit(): if not series_index.replace('.', '', 1).isdigit():
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
return False return False
if str(book.series_index) != series_index: if str(book.series_index) != series_index:
book.series_index = series_index book.series_index = series_index
modif_date = True modify_date = True
return modif_date return modify_date
# Handle book comments/description # Handle book comments/description
def edit_book_comments(comments, book): def edit_book_comments(comments, book):
modif_date = False modify_date = False
if comments: if comments:
comments = clean_html(comments) comments = clean_html(comments)
if len(book.comments): if len(book.comments):
if book.comments[0].text != comments: if book.comments[0].text != comments:
book.comments[0].text = comments book.comments[0].text = comments
modif_date = True modify_date = True
else: else:
if comments: if comments:
book.comments.append(db.Comments(text=comments, book=book.id)) book.comments.append(db.Comments(comment=comments, book=book.id))
modif_date = True modify_date = True
return modif_date 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(',') input_languages = languages.split(',')
unknown_languages = [] unknown_languages = []
if not upload: if not upload_mode:
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages) input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
else: else:
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages: for lang in unknown_languages:
log.error("'%s' is not a valid language", l) log.error("'%s' is not a valid language", lang)
if isinstance(invalid, list): if isinstance(invalid, list):
invalid.append(l) invalid.append(lang)
else: 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 # 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 # 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 # 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": 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) getattr(book, cc_string).append(new_cc)
return changed, to_save return changed, to_save
def edit_single_cc_data(book_id, book, column_id, to_save): def edit_single_cc_data(book_id, book, column_id, to_save):
cc = (calibre_db.session.query(db.Custom_Columns) cc = (calibre_db.session.query(db.CustomColumns)
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)) .filter(db.CustomColumns.datatype.notin_(db.cc_exceptions))
.filter(db.Custom_Columns.id == column_id) .filter(db.CustomColumns.id == column_id)
.all()) .all())
return edit_cc_data(book_id, book, to_save, cc) return edit_cc_data(book_id, book, to_save, cc)
def edit_all_cc_data(book_id, book, to_save): 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) return edit_cc_data(book_id, book, to_save, cc)
def edit_cc_data(book_id, book, to_save, cc): def edit_cc_data(book_id, book, to_save, cc):
changed = False changed = False
for c in cc: for c in cc:
@ -614,10 +622,11 @@ def edit_cc_data(book_id, book, to_save, cc):
'custom') 'custom')
return changed return changed
def upload_single_file(request, book, book_id):
def upload_single_file(file_request, book, book_id):
# Check and handle Uploaded file # Check and handle Uploaded file
if 'btn-upload-format' in request.files: if 'btn-upload-format' in file_request.files:
requested_file = request.files['btn-upload-format'] requested_file = file_request.files['btn-upload-format']
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
@ -669,17 +678,17 @@ def upload_single_file(request, book, book_id):
# Queue uploader info # Queue uploader info
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) 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) upload_text = _(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))) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
return uploader.process( return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename), saved_filename, *os.path.splitext(requested_file.filename),
rarExecutable=config.config_rarfile_location) rarExecutable=config.config_rarfile_location)
def upload_cover(request, book): def upload_cover(cover_request, book):
if 'btn-upload-cover' in request.files: if 'btn-upload-cover' in cover_request.files:
requested_file = request.files['btn-upload-cover'] requested_file = cover_request.files['btn-upload-cover']
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): 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): def handle_author_on_edit(book, author_name, update_stored=True):
# handle author(s) # handle author(s)
# renamed = False input_authors, renamed = prepare_authors(author_name)
input_authors = author_name.split('&') '''input_authors = author_name.split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
# Remove duplicates in authors list # Remove duplicates in authors list
input_authors = helper.uniq(input_authors) 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_renamed_author = helper.get_sorted_author(renamed_author.name)
sorted_old_author = helper.get_sorted_author(in_aut) sorted_old_author = helper.get_sorted_author(in_aut)
for one_book in all_books: 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') 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 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 @login_required_if_no_ano
@edit_required @edit_required
def edit_book(book_id): def edit_book(book_id):
modif_date = False modify_date = False
# create the function for sorting... # create the function for sorting...
try: try:
@ -767,13 +776,14 @@ def edit_book(book_id):
# Book not found # Book not found
if not book: 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")) return redirect(url_for("web.index"))
meta = upload_single_file(request, book, book_id) meta = upload_single_file(request, book, book_id)
if upload_cover(request, book) is True: if upload_cover(request, book) is True:
book.has_cover = 1 book.has_cover = 1
modif_date = True modify_date = True
try: try:
to_save = request.form.to_dict() to_save = request.form.to_dict()
merge_metadata(to_save, meta) 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"]) input_authors, authorchange, renamed = handle_author_on_edit(book, to_save["author_name"])
if authorchange or title_change: if authorchange or title_change:
edited_books_id = book.id edited_books_id = book.id
modif_date = True modify_date = True
if config.config_use_google_drive: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
error = False error = ""
if edited_books_id: if edited_books_id:
error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], 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 not error:
if "cover_url" in to_save: 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) result, error = helper.save_cover_from_url(to_save["cover_url"], book.path)
if result is True: if result is True:
book.has_cover = 1 book.has_cover = 1
modif_date = True modify_date = True
else: else:
flash(error, category="error") flash(error, category="error")
# Add default series_index to book # 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 # 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 # Handle identifiers
input_identifiers = identifier_list(to_save, book) input_identifiers = identifier_list(to_save, book)
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
if warning: if warning:
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning") flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
modif_date |= modification modify_date |= modification
# Handle book tags # Handle book tags
modif_date |= edit_book_tags(to_save['tags'], book) modify_date |= edit_book_tags(to_save['tags'], book)
# Handle book series # Handle book series
modif_date |= edit_book_series(to_save["series"], book) modify_date |= edit_book_series(to_save["series"], book)
# handle book publisher # handle book publisher
modif_date |= edit_book_publisher(to_save['publisher'], book) modify_date |= edit_book_publisher(to_save['publisher'], book)
# handle book languages # handle book languages
modif_date |= edit_book_languages(to_save['languages'], book) modify_date |= edit_book_languages(to_save['languages'], book)
# handle book ratings # handle book ratings
modif_date |= edit_book_ratings(to_save, book) modify_date |= edit_book_ratings(to_save, book)
# handle cc data # 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"]: if to_save["pubdate"]:
try: try:
@ -843,7 +853,7 @@ def edit_book(book_id):
else: else:
book.pubdate = db.Books.DEFAULT_PUBDATE book.pubdate = db.Books.DEFAULT_PUBDATE
if modif_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id, all=True) kobo_sync_status.remove_synced_book(edited_books_id, all=True)
@ -905,14 +915,7 @@ def identifier_list(to_save, book):
return result return result
def prepare_authors_on_upload(title, authr): def prepare_authors(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")
# handle authors # handle authors
input_authors = authr.split('&') input_authors = authr.split('&')
# handle_authors(input_authors) # handle_authors(input_authors)
@ -935,6 +938,18 @@ def prepare_authors_on_upload(title, authr):
sorted_old_author = helper.get_sorted_author(in_aut) sorted_old_author = helper.get_sorted_author(in_aut)
for one_book in all_books: 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)
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() sort_authors_list = list()
db_author = None db_author = None
@ -955,7 +970,7 @@ def prepare_authors_on_upload(title, authr):
return sort_authors, input_authors, db_author, renamed 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 title = meta.title
authr = meta.author authr = meta.author
sort_authors, input_authors, db_author, renamed_authors = prepare_authors_on_upload(title, authr) 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) title_dir = helper.get_valid_filename(title, chars=96)
author_dir = helper.get_valid_filename(db_author.name, 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('\\', '/') path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone # Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1), db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
'1', datetime.utcnow(), path, meta.cover, db_author, [], "") '1', datetime.utcnow(), path, meta.cover, db_author, [], "")
modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session, modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
'author') 'author')
# Add series_index to book # 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 # add languages
invalid=[] invalid = []
modif_date |= edit_book_languages(meta.languages, db_book, upload=True, invalid=invalid) modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
if invalid: if invalid:
for l in invalid: for lang in invalid:
flash(_(u"'%(langname)s' is not a valid language", langname=l), category="warning") flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning")
# handle tags # handle tags
modif_date |= edit_book_tags(meta.tags, db_book) modify_date |= edit_book_tags(meta.tags, db_book)
# handle publisher # handle publisher
modif_date |= edit_book_publisher(meta.publisher, db_book) modify_date |= edit_book_publisher(meta.publisher, db_book)
# handle series # handle series
modif_date |= edit_book_series(meta.series, db_book) modify_date |= edit_book_series(meta.series, db_book)
# Add file to book # Add file to book
file_size = os.path.getsize(meta.file_path) file_size = os.path.getsize(meta.file_path)
@ -1002,6 +1017,7 @@ def create_book_on_upload(modif_date, meta):
calibre_db.session.flush() calibre_db.session.flush()
return db_book, input_authors, title_dir, renamed_authors return db_book, input_authors, title_dir, renamed_authors
def file_handling_on_upload(requested_file): def file_handling_on_upload(requested_file):
# check if file extension is correct # check if file extension is correct
if '.' in requested_file.filename: if '.' in requested_file.filename:
@ -1045,7 +1061,7 @@ def move_coverfile(meta, db_book):
category="error") category="error")
@editbook.route("/upload", methods=["POST"]) @EditBook.route("/upload", methods=["POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
def upload(): def upload():
@ -1054,7 +1070,7 @@ def upload():
if request.method == 'POST' and 'btn-upload' in request.files: if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"): for requested_file in request.files.getlist("btn-upload"):
try: try:
modif_date = False modify_date = False
# create the function for sorting... # create the function for sorting...
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
@ -1063,10 +1079,10 @@ def upload():
if error: if error:
return 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 # Comments need book id therefore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book) modify_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
book_id = db_book.id book_id = db_book.id
title = db_book.title title = db_book.title
@ -1096,12 +1112,12 @@ def upload():
if error: if error:
flash(error, category="error") flash(error, category="error")
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title)) link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
uploadText = _(u"File %(file)s uploaded", file=link) upload_text = _(u"File %(file)s uploaded", file=link)
WorkerThread.add(current_user.name, TaskUpload(uploadText, escape(title))) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
if len(request.files.getlist("btn-upload")) < 2: if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin(): 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') return Response(json.dumps(resp), mimetype='application/json')
else: else:
resp = {"location": url_for('web.show_book', book_id=book_id)} 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') 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 @login_required_if_no_ano
@edit_required @edit_required
def convert_bookformat(book_id): 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): if (book_format_from is None) or (book_format_to is None):
flash(_(u"Source or destination format for conversion missing"), category="error") 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) 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(), 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: if rtn is None:
flash(_(u"Book successfully queued for converting to %(book_format)s", flash(_(u"Book successfully queued for converting to %(book_format)s",
book_format=book_format_to), book_format=book_format_to),
category="success") category="success")
else: else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") 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 @login_required
def table_get_custom_enum(c_id): def table_get_custom_enum(c_id):
ret = list() ret = list()
cc = (calibre_db.session.query(db.Custom_Columns) cc = (calibre_db.session.query(db.CustomColumns)
.filter(db.Custom_Columns.id == c_id) .filter(db.CustomColumns.id == c_id)
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none()) .filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).one_or_none())
ret.append({'value': "", 'text': ""}) ret.append({'value': "", 'text': ""})
for idx, en in enumerate(cc.get_display_dict()['enum_values']): for idx, en in enumerate(cc.get_display_dict()['enum_values']):
ret.append({'value': en, 'text': en}) ret.append({'value': en, 'text': en})
return json.dumps(ret) return json.dumps(ret)
@editbook.route("/ajax/editbooks/<param>", methods=['POST']) @EditBook.route("/ajax/editbooks/<param>", methods=['POST'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
def edit_list_book(param): def edit_list_book(param):
vals = request.form.to_dict() vals = request.form.to_dict()
book = calibre_db.get_book(vals['pk']) book = calibre_db.get_book(vals['pk'])
sort_param = ""
# ret = "" # ret = ""
try: try:
if param == 'series_index': if param == 'series_index':
@ -1172,7 +1190,7 @@ def edit_list_book(param):
elif param == 'publishers': elif param == 'publishers':
edit_book_publisher(vals['value'], book) edit_book_publisher(vals['value'], book)
ret = Response(json.dumps({'success': True, 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') mimetype='application/json')
elif param == 'languages': elif param == 'languages':
invalid = list() invalid = list()
@ -1186,13 +1204,13 @@ def edit_list_book(param):
for lang in book.languages: for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json') mimetype='application/json')
elif param == 'author_sort': elif param == 'author_sort':
book.author_sort = vals['value'] book.author_sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}), ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
mimetype='application/json') mimetype='application/json')
elif param == 'title': elif param == 'title':
sort = book.sort sort_param = book.sort
handle_title_on_edit(book, vals.get('value', "")) handle_title_on_edit(book, vals.get('value', ""))
helper.update_dir_structure(book.id, config.config_calibre_dir) helper.update_dir_structure(book.id, config.config_calibre_dir)
ret = Response(json.dumps({'success': True, 'newValue': book.title}), ret = Response(json.dumps({'success': True, 'newValue': book.title}),
@ -1208,12 +1226,13 @@ def edit_list_book(param):
elif param == 'authors': elif param == 'authors':
input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") 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) helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], renamed_author=renamed)
ret = Response(json.dumps({'success': True, ret = Response(json.dumps({
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}), 'success': True,
mimetype='application/json') 'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
mimetype='application/json')
elif param == 'is_archived': elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True", 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: if is_archived:
kobo_sync_status.remove_synced_book(book.id) kobo_sync_status.remove_synced_book(book.id)
return "" return ""
@ -1238,7 +1257,7 @@ def edit_list_book(param):
calibre_db.session.commit() calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated # revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == "false": if param == 'title' and vals.get('checkT') == "false":
book.sort = sort book.sort = sort_param
calibre_db.session.commit() calibre_db.session.commit()
except (OperationalError, IntegrityError) as e: except (OperationalError, IntegrityError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
@ -1249,7 +1268,7 @@ def edit_list_book(param):
return ret return ret
@editbook.route("/ajax/sort_value/<field>/<int:bookid>") @EditBook.route("/ajax/sort_value/<field>/<int:bookid>")
@login_required @login_required
def get_sorted_entry(field, bookid): def get_sorted_entry(field, bookid):
if field in ['title', 'authors', 'sort', 'author_sort']: if field in ['title', 'authors', 'sort', 'author_sort']:
@ -1266,7 +1285,7 @@ def get_sorted_entry(field, bookid):
return "" return ""
@editbook.route("/ajax/simulatemerge", methods=['POST']) @EditBook.route("/ajax/simulatemerge", methods=['POST'])
@login_required @login_required
@edit_required @edit_required
def simulate_merge_list_book(): def simulate_merge_list_book():
@ -1282,7 +1301,7 @@ def simulate_merge_list_book():
return "" return ""
@editbook.route("/ajax/mergebooks", methods=['POST']) @EditBook.route("/ajax/mergebooks", methods=['POST'])
@login_required @login_required
@edit_required @edit_required
def merge_list_book(): def merge_list_book():
@ -1295,8 +1314,9 @@ def merge_list_book():
if to_book: if to_book:
for file in to_book.data: for file in to_book.data:
to_file.append(file.format) to_file.append(file.format)
to_name = helper.get_valid_filename(to_book.title, chars=96) + ' - ' + \ to_name = helper.get_valid_filename(to_book.title,
helper.get_valid_filename(to_book.authors[0].name, chars=96) chars=96) + ' - ' + helper.get_valid_filename(to_book.authors[0].name,
chars=96)
for book_id in vals: for book_id in vals:
from_book = calibre_db.get_book(book_id) from_book = calibre_db.get_book(book_id)
if from_book: if from_book:
@ -1314,19 +1334,20 @@ def merge_list_book():
element.format, element.format,
element.uncompressed_size, element.uncompressed_size,
to_name)) to_name))
delete_book_from_table(from_book.id,"", True) delete_book_from_table(from_book.id, "", True)
return json.dumps({'success': True}) return json.dumps({'success': True})
return "" return ""
@editbook.route("/ajax/xchange", methods=['POST']) @EditBook.route("/ajax/xchange", methods=['POST'])
@login_required @login_required
@edit_required @edit_required
def table_xchange_author_title(): def table_xchange_author_title():
vals = request.get_json().get('xchange') vals = request.get_json().get('xchange')
edited_books_id = False
if vals: if vals:
for val in vals: for val in vals:
modif_date = False modify_date = False
book = calibre_db.get_book(val) book = calibre_db.get_book(val)
authors = book.title authors = book.title
book.authors = calibre_db.order_authors([book]) 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) input_authors, authorchange, renamed = handle_author_on_edit(book, authors)
if authorchange or title_change: if authorchange or title_change:
edited_books_id = book.id edited_books_id = book.id
modif_date = True modify_date = True
if config.config_use_google_drive: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
if edited_books_id: if edited_books_id:
helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0],
renamed_author=renamed) renamed_author=renamed)
if modif_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
try: try:
calibre_db.session.commit() 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') txt = epub_zip.read('META-INF/container.xml')
tree = etree.fromstring(txt) tree = etree.fromstring(txt)
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epub_zip.read(cfname) cf = epub_zip.read(cf_name)
tree = etree.fromstring(cf) 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] 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) 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']: if not epub_metadata['title']:
title = original_file_name 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): 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_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
cover_file = None cover_file = None
if len(cover_section) > 0: # if len(cover_section) > 0:
cover_file = _extract_cover(epub_zip, cover_section[0], cover_path, tmp_file_path) for cs in cover_section:
else: 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) meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0: if len(meta_cover) > 0:
cover_section = tree.xpath( 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) cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else: else:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path) cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file: if cover_file: break
break
return cover_file return cover_file

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

@ -28,7 +28,6 @@ from flask import Blueprint, request, render_template, Response, g, make_respons
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_, true from sqlalchemy.sql.expression import func, text, or_, and_, true
from werkzeug.security import check_password_hash 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 . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
from .helper import get_download_link, get_book_cover from .helper import get_download_link, get_book_cover
from .pagination import Pagination from .pagination import Pagination
@ -99,26 +98,7 @@ def feed_normal_search():
@opds.route("/opds/books") @opds.route("/opds/books")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_booksindex(): def feed_booksindex():
shift = 0 return render_element_index(db.Books.sort, None, 'opds.feed_letter_books')
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)
@opds.route("/opds/books/letter/<book_id>") @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) hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list() entries = list()
for book in hot_books: for book in hot_books:
downloadBook = calibre_db.get_book(book.Downloads.book_id) download_book = calibre_db.get_book(book.Downloads.book_id)
if downloadBook: if download_book:
entries.append( entries.append(
calibre_db.get_filtered_book(book.Downloads.book_id) calibre_db.get_filtered_book(book.Downloads.book_id)
) )
else: else:
ub.delete_download(book.Downloads.book_id) 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), 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) return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/author") @opds.route("/opds/author")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_authorindex(): def feed_authorindex():
shift = 0 return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
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)
@opds.route("/opds/author/letter/<book_id>") @opds.route("/opds/author/letter/<book_id>")
@ -228,12 +188,7 @@ def feed_letter_author(book_id):
@opds.route("/opds/author/<int:book_id>") @opds.route("/opds/author/<int:book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_author(book_id): def feed_author(book_id):
off = request.args.get("offset") or 0 return render_xml_dataset(db.Authors, book_id)
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)
@opds.route("/opds/publisher") @opds.route("/opds/publisher")
@ -254,37 +209,14 @@ def feed_publisherindex():
@opds.route("/opds/publisher/<int:book_id>") @opds.route("/opds/publisher/<int:book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_publisher(book_id): def feed_publisher(book_id):
off = request.args.get("offset") or 0 return render_xml_dataset(db.Publishers, book_id)
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)
@opds.route("/opds/category") @opds.route("/opds/category")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_categoryindex(): def feed_categoryindex():
shift = 0 return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
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})
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>") @opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
@ -306,36 +238,14 @@ def feed_letter_category(book_id):
@opds.route("/opds/category/<int:book_id>") @opds.route("/opds/category/<int:book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_category(book_id): def feed_category(book_id):
off = request.args.get("offset") or 0 return render_xml_dataset(db.Tags, book_id)
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)
@opds.route("/opds/series") @opds.route("/opds/series")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_seriesindex(): def feed_seriesindex():
shift = 0 return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
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)
@opds.route("/opds/series/letter/<book_id>") @opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
@ -370,7 +280,7 @@ def feed_series(book_id):
def feed_ratingindex(): def feed_ratingindex():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), 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_ratings_link)\
.join(db.Books)\ .join(db.Books)\
.filter(calibre_db.common_filters()) \ .filter(calibre_db.common_filters()) \
@ -388,12 +298,7 @@ def feed_ratingindex():
@opds.route("/opds/ratings/<book_id>") @opds.route("/opds/ratings/<book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_ratings(book_id): def feed_ratings(book_id):
off = request.args.get("offset") or 0 return render_xml_dataset(db.Tags, book_id)
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)
@opds.route("/opds/formats") @opds.route("/opds/formats")
@ -491,7 +396,7 @@ def feed_shelf(book_id):
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def opds_download_link(book_id, book_format): 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 # 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 # in case of anonymous browsing user is None
user = load_user_from_request(request) or current_user user = load_user_from_request(request) or current_user
if not user.role_download(): if not user.role_download():
@ -517,6 +422,31 @@ def get_metadata_calibre_companion(uuid, library):
return "" 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): def feed_search(term):
if term: if term:
entries, __, ___ = calibre_db.get_search_results(term, config_read_column=config.config_read_column) entries, __, ___ = calibre_db.get_search_results(term, config_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)): if bool(user and check_password_hash(str(user.password), password)):
return True return True
else: else:
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr) 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) log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_address)
return False return False
@ -559,26 +489,33 @@ def render_xml_template(*args, **kwargs):
return response return response
@opds.route("/opds/thumb_240_240/<book_id>") def render_xml_dataset(data_table, 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 off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
return render_xml_template('feed.xml', entries=result, pagination=pagination) 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") def render_element_index(database_column, linked_table, folder):
@requires_basic_auth_if_no_ano shift = 0
def feed_unread_books(): off = int(request.args.get("offset") or 0)
off = request.args.get("offset") or 0 entries = calibre_db.session.query(func.upper(func.substr(database_column, 1, 1)).label('id'))
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) if linked_table:
return render_xml_template('feed.xml', entries=result, pagination=pagination) 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): def has_next(self):
return self.page < self.pages 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 # 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 shwn # 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 shwn # 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 shwn # 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, def iter_pages(self, left_edge=2, left_current=2,
right_current=4, right_edge=2): right_current=4, right_edge=2):
last = 0 last = 0

@ -22,6 +22,7 @@
import json import json
from datetime import datetime from datetime import datetime
from functools import wraps
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
from flask_login import login_required, current_user, login_user 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 . import config, logger, ub
from .render_template import render_title_template 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__) remotelogin = Blueprint('remotelogin', __name__)
log = logger.create() log = logger.create()

@ -22,7 +22,7 @@
{% if source_formats|length > 0 and conversion_formats|length > 0 %} {% if source_formats|length > 0 and conversion_formats|length > 0 %}
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4> <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() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group"> <div class="form-group">
<div class="text-left"> <div class="text-left">
@ -48,7 +48,7 @@
{% endif %} {% endif %}
</div> </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() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-sm-9 col-xs-12"> <div class="col-sm-9 col-xs-12">
<div class="form-group"> <div class="form-group">

@ -6,7 +6,7 @@
data-escape="true" data-escape="true"
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
data-editable-type="text" 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-editable-title="{{ edit_text }}"
data-edit="true" data-edit="true"
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}" {% endif %} {% 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('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }} {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), 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) }} {{ 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--> <!--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) }} {{ 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) %} {% if g.user.check_visibility(32768) %}
{{ book_checkbox_row('is_archived', _('Archiv Status'), false)}} {{ book_checkbox_row('is_archived', _('Archiv Status'), false)}}
{% endif %} {% endif %}
{{ book_checkbox_row('read_status', _('Read Status'), false)}} {{ book_checkbox_row('read_status', _('Read Status'), false)}}
{% for c in cc %} {% for c in cc %}
{% if c.datatype == "int" %} {% 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" %} {% 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" %} {% 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" %} {% 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"] %} {% elif c.datatype in ["datetime"] %}
<!-- missing --> <!-- missing -->
{% elif c.datatype == "text" %} {% elif c.datatype == "text" %}
{{ text_table_row('custom_column_' + c.id|string, _('Enter ') + c.name, c.name, false, false) }} {{ text_table_row('custom_column_' + c.id|string, _('Enter ') + c.name, c.name, false, false) }}
{% elif c.datatype == "comments" %} {% 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" %} {% elif c.datatype == "bool" %}
{{ book_checkbox_row('custom_column_' + c.id|string, c.name, false)}} {{ book_checkbox_row('custom_column_' + c.id|string, c.name, false)}}
{% else %} {% else %}

@ -138,7 +138,7 @@
<p> <p>
<span class="glyphicon glyphicon-link"></span> <span class="glyphicon glyphicon-link"></span>
{% for identifier in entry.identifiers %} {% 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%} {%endfor%}
</p> </p>
</div> </div>
@ -295,7 +295,7 @@
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book"> <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>
</div> </div>
{% endif %} {% endif %}

@ -60,7 +60,7 @@
{% if g.user.is_authenticated or g.allow_anonymous %} {% if g.user.is_authenticated or g.allow_anonymous %}
{% if g.user.role_upload() and g.allow_upload %} {% if g.user.role_upload() and g.allow_upload %}
<li> <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() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group"> <div class="form-group">
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload" <span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"

@ -18,6 +18,7 @@
import base64 import base64
import binascii import binascii
from functools import wraps
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash 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 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): def login_required_if_no_ano(func):
@wraps(func) @wraps(func)

@ -29,7 +29,7 @@ import copy
from functools import wraps from functools import wraps
from babel.dates import format_date from babel.dates import format_date
from babel import Locale as LC from babel import Locale
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session 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 .render_template import render_title_template
from .kobo_sync_status import change_archived_books from .kobo_sync_status import change_archived_books
feature_support = { feature_support = {
'ldap': bool(services.ldap), 'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support), 'goodreads': bool(services.goodreads_support),
@ -69,10 +68,12 @@ feature_support = {
try: try:
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
feature_support['oauth'] = True feature_support['oauth'] = True
except ImportError: except ImportError:
feature_support['oauth'] = False feature_support['oauth'] = False
oauth_check = {} oauth_check = {}
register_user_with_oauth = logout_oauth_user = get_oauth_status = None
try: try:
from natsort import natsorted as sort from natsort import natsorted as sort
@ -82,8 +83,11 @@ except ImportError:
@app.after_request @app.after_request
def add_security_headers(resp): 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:" csp = "default-src 'self'"
if request.endpoint == "editbook.edit_book" or config.config_use_google_drive: 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'] += " *" resp.headers['Content-Security-Policy'] += " *"
elif request.endpoint == "web.read_book": elif request.endpoint == "web.read_book":
resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';" 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;' resp.headers['Strict-Transport-Security'] = 'max-age=31536000;'
return resp return resp
web = Blueprint('web', __name__) web = Blueprint('web', __name__)
log = logger.create() log = logger.create()
@ -119,6 +124,7 @@ def viewer_required(f):
return inner return inner
# ################################### data provider functions ######################################################### # ################################### data provider functions #########################################################
@ -140,11 +146,11 @@ def set_bookmark(book_id, book_format):
ub.session_commit() ub.session_commit()
return "", 204 return "", 204
lbookmark = ub.Bookmark(user_id=current_user.id, l_bookmark = ub.Bookmark(user_id=current_user.id,
book_id=book_id, book_id=book_id,
format=book_format, format=book_format,
bookmark_key=bookmark_key) bookmark_key=bookmark_key)
ub.session.merge(lbookmark) ub.session.merge(l_bookmark)
ub.session_commit("Bookmark for user {} in book {} created".format(current_user.id, book_id)) ub.session_commit("Bookmark for user {} in book {} created".format(current_user.id, book_id))
return "", 201 return "", 201
@ -162,7 +168,7 @@ def toggle_read(book_id):
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST']) @web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
@login_required @login_required
def toggle_archived(book_id): 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: if is_archived:
remove_synced_book(book_id) remove_synced_book(book_id)
return "" return ""
@ -230,6 +236,7 @@ def get_comic_book(book_id, book_format, page):
return "", 204 return "", 204
''' '''
# ################################### Typeahead ################################################################## # ################################### Typeahead ##################################################################
@ -297,6 +304,12 @@ def get_matching_tags():
return json_dumps 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): def get_sort_function(sort_param, data):
order = [db.Books.timestamp.desc()] order = [db.Books.timestamp.desc()]
if sort_param == 'stored': if sort_param == 'stored':
@ -373,7 +386,7 @@ def render_books_list(data, sort_param, book_id, page):
else: else:
website = data or "newest" website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0], entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0],
False, 0, False, 0,
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book, db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
@ -407,12 +420,13 @@ def render_discover_books(page, book_id):
else: else:
abort(404) abort(404)
def render_hot_books(page, order): def render_hot_books(page, order):
if current_user.check_visibility(constants.SIDEBAR_HOT): if current_user.check_visibility(constants.SIDEBAR_HOT):
if order[1] not in ['hotasc', 'hotdesc']: if order[1] not in ['hotasc', 'hotdesc']:
# Unary expression comparsion only working (for this expression) in sqlalchemy 1.4+ # 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 # 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[0][0].compare(func.count(ub.Downloads.book_id).asc())):
order = [func.count(ub.Downloads.book_id).desc()], 'hotdesc' order = [func.count(ub.Downloads.book_id).desc()], 'hotdesc'
if current_user.show_detail_random(): if current_user.show_detail_random():
random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
@ -420,19 +434,19 @@ def render_hot_books(page, order):
else: else:
random = false() random = false()
off = int(int(config.config_books_per_page) * (page - 1)) 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) .order_by(*order[0]).group_by(ub.Downloads.book_id)
hot_books = all_books.offset(off).limit(config.config_books_per_page) hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list() entries = list()
for book in hot_books: 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() db.Books.id == book.Downloads.book_id).first()
if downloadBook: if download_book:
entries.append(downloadBook) entries.append(download_book)
else: else:
ub.delete_download(book.Downloads.book_id) ub.delete_download(book.Downloads.book_id)
numBooks = entries.__len__() num_books = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks) pagination = Pagination(page, config.config_books_per_page, num_books)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Hot Books (Most Downloaded)"), page="hot", order=order[1]) title=_(u"Hot Books (Most Downloaded)"), page="hot", order=order[1])
else: else:
@ -462,8 +476,8 @@ def render_downloaded_books(page, order, user_id):
db.Series, db.Series,
ub.Downloads, db.Books.id == ub.Downloads.book_id) ub.Downloads, db.Books.id == ub.Downloads.book_id)
for book in entries: for book in entries:
if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ if not calibre_db.session.query(db.Books).\
.filter(db.Books.id == book.id).first(): filter(calibre_db.common_filters()).filter(db.Books.id == book.id).first():
ub.delete_download(book.id) ub.delete_download(book.id)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first() user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html', return render_title_template('index.html',
@ -471,7 +485,7 @@ def render_downloaded_books(page, order, user_id):
entries=entries, entries=entries,
pagination=pagination, pagination=pagination,
id=user_id, 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", page="download",
order=order[1]) order=order[1])
else: else:
@ -639,29 +653,27 @@ def render_read_books(page, are_read, as_xml=False, order=None):
column=config.config_read_column), column=config.config_read_column),
category="error") category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
return [] # ToDo: Handle error Case for opds return [] # ToDo: Handle error Case for opds
if as_xml: if as_xml:
return entries, pagination return entries, pagination
else: else:
if are_read: if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')' name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read" page_name = "read"
else: else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')' 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, 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): def render_archived_books(page, sort_param):
order = sort_param[0] or [] order = sort_param[0] or []
archived_books = ( archived_books = (ub.session.query(ub.ArchivedBook)
ub.session.query(ub.ArchivedBook) .filter(ub.ArchivedBook.user_id == int(current_user.id))
.filter(ub.ArchivedBook.user_id == int(current_user.id)) .filter(ub.ArchivedBook.is_archived == True)
.filter(ub.ArchivedBook.is_archived == True) .all())
.all()
)
archived_book_ids = [archived_book.book_id for archived_book in archived_books] archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.in_(archived_book_ids) archived_filter = db.Books.id.in_(archived_book_ids)
@ -674,40 +686,40 @@ def render_archived_books(page, sort_param):
False, 0) False, 0)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')' 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, 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): def render_prepare_search_form(cc):
# prepare data for search-form # prepare data for search-form
tags = calibre_db.session.query(db.Tags)\ tags = calibre_db.session.query(db.Tags) \
.join(db.books_tags_link)\ .join(db.books_tags_link) \
.join(db.Books)\ .join(db.Books) \
.filter(calibre_db.common_filters()) \ .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() .order_by(db.Tags.name).all()
series = calibre_db.session.query(db.Series)\ series = calibre_db.session.query(db.Series) \
.join(db.books_series_link)\ .join(db.books_series_link) \
.join(db.Books)\ .join(db.Books) \
.filter(calibre_db.common_filters()) \ .filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series'))\ .group_by(text('books_series_link.series')) \
.order_by(db.Series.name)\ .order_by(db.Series.name) \
.filter(calibre_db.common_filters()).all() .filter(calibre_db.common_filters()).all()
shelves = ub.session.query(ub.Shelf)\ shelves = ub.session.query(ub.Shelf) \
.filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\ .filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id))) \
.order_by(ub.Shelf.name).all() .order_by(ub.Shelf.name).all()
extensions = calibre_db.session.query(db.Data)\ extensions = calibre_db.session.query(db.Data) \
.join(db.Books)\ .join(db.Books) \
.filter(calibre_db.common_filters()) \ .filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\ .group_by(db.Data.format) \
.order_by(db.Data.format).all() .order_by(db.Data.format).all()
if current_user.filter_language() == u"all": if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
else: else:
languages = None languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions, 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): 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, offset,
order, order,
limit, limit,
False,
config.config_read_column, config.config_read_column,
*join) *join)
return render_title_template('search.html', 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", return render_title_template('book_table.html', title=_(u"Books List"), cc=cc, page="book_table",
visiblility=visibility) visiblility=visibility)
@web.route("/ajax/listbooks") @web.route("/ajax/listbooks")
@login_required @login_required
def list_books(): def list_books():
off = int(request.args.get("offset") or 0) off = int(request.args.get("offset") or 0)
limit = int(request.args.get("limit") or config.config_books_per_page) 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") sort_param = request.args.get("sort", "id")
order = request.args.get("order", "").lower() order = request.args.get("order", "").lower()
state = None state = None
@ -784,8 +796,8 @@ def list_books():
elif sort_param == "authors": elif sort_param == "authors":
order = [db.Authors.name.asc(), db.Series.name, db.Books.series_index] if order == "asc" \ 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()] 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, \ join = db.books_authors_link, db.Books.id == db.books_authors_link.c.book, db.Authors, db.books_series_link, \
db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series db.Books.id == db.books_series_link.c.book, db.Series
elif sort_param == "languages": elif sort_param == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()] 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 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: elif not state:
order = [db.Books.timestamp.desc()] 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 state is not None:
if search: if search_param:
books = calibre_db.search_query(search, config.config_read_column).all() books = calibre_db.search_query(search_param, config.config_read_column).all()
filtered_count = len(books) filtered_count = len(books)
else: else:
if not config.config_read_column: if not config.config_read_column:
@ -818,15 +831,14 @@ def list_books():
# Skip linking read column and return None instead of read status # Skip linking read column and return None instead of read status
books = calibre_db.session.query(db.Books, None, ub.ArchivedBook.is_archived) 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, 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()) .filter(calibre_db.common_filters(allow_show_archived=True)).all())
entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order, True) entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order, True)
elif search: elif search_param:
entries, filtered_count, __ = calibre_db.get_search_results(search, entries, filtered_count, __ = calibre_db.get_search_results(search_param,
off, off,
[order,''], [order, ''],
limit, limit,
True,
config.config_read_column, config.config_read_column,
*join) *join)
else: else:
@ -845,9 +857,9 @@ def list_books():
val = entry[0] val = entry[0]
val.read_status = entry[1] == ub.ReadBook.STATUS_FINISHED val.read_status = entry[1] == ub.ReadBook.STATUS_FINISHED
val.is_archived = entry[2] is True val.is_archived = entry[2] is True
for index in range(0, len(val.languages)): for lang_index in range(0, len(val.languages)):
val.languages[index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[ val.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[
index].lang_code) lang_index].lang_code)
result.append(val) result.append(val)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": result} 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" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response return response
@web.route("/ajax/table_settings", methods=['POST']) @web.route("/ajax/table_settings", methods=['POST'])
@login_required @login_required
def update_table_settings(): 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')) \ 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()) \ .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(order).all() .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')) \ char_list = generate_char_list(db.Authors.sort, db.books_authors_link)
.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()
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name # If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
# starts a change session # starts a change session
autor_copy = copy.deepcopy(entries) author_copy = copy.deepcopy(entries)
for entry in autor_copy: for entry in author_copy:
entry.Authors.name = entry.Authors.name.replace('|', ',') 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) title=u"Authors", page="authorlist", data='author', order=order_no)
else: else:
abort(404) abort(404)
@web.route("/downloadlist") @web.route("/downloadlist")
@login_required_if_no_ano @login_required_if_no_ano
def download_list(): def download_list():
@ -909,12 +921,12 @@ def download_list():
order = ub.User.name.asc() order = ub.User.name.asc()
order_no = 1 order_no = 1
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and current_user.role_admin(): 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() .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) \ .filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) \
.group_by(func.upper(func.substr(ub.User.name, 1, 1))).all() .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) title=_(u"Downloads"), page="downloadlist", data="download", order=order_no)
else: else:
abort(404) abort(404)
@ -933,10 +945,8 @@ def publisher_list():
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \ 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()) \ .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_publishers_link.publisher')).order_by(order).all() .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')) \ char_list = generate_char_list(db.Publishers.name, db.books_publishers_link)
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
.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,
title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no) title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
else: else:
abort(404) abort(404)
@ -952,25 +962,19 @@ def series_list():
else: else:
order = db.Series.sort.asc() order = db.Series.sort.asc()
order_no = 1 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': 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')) \ 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()) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(order).all() .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')) \ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
.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,
title=_(u"Series"), page="serieslist", data="series", order=order_no) title=_(u"Series"), page="serieslist", data="series", order=order_no)
else: else:
entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count'), entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count'),
func.max(db.Books.series_index), db.Books.id) \ 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() .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')) \ return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
.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,
title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view", title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no) order=order_no)
else: else:
@ -988,7 +992,7 @@ def ratings_list():
order = db.Ratings.rating.asc() order = db.Ratings.rating.asc()
order_no = 1 order_no = 1
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), 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()) \ .join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(order).all() .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(), return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
@ -1023,14 +1027,14 @@ def formats_list():
def language_overview(): def language_overview():
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == u"all": 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 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) languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
for lang in languages: for lang in languages:
upper_lang = lang[0].name[0].upper() upper_lang = lang[0].name[0].upper()
if upper_lang not in charlist: if upper_lang not in char_list:
charlist.append(upper_lang) char_list.append(upper_lang)
return render_title_template('languages.html', languages=languages, 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) data="language", order=order_no)
else: else:
abort(404) abort(404)
@ -1049,10 +1053,8 @@ def category_list():
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \ 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()) \ .join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag')).all() .group_by(text('books_tags_link.tag')).all()
charlist = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('char')) \ char_list = generate_char_list(db.Tags.name, db.books_tags_link)
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
.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,
title=_(u"Categories"), page="catlist", data="category", order=order_no) title=_(u"Categories"), page="catlist", data="category", order=order_no)
else: else:
abort(404) abort(404)
@ -1176,7 +1178,15 @@ def adv_search_read_status(q, read_status):
return q 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: for extension in include_extension_inputs:
q = q.filter(db.Books.data.any(db.Data.format == extension)) q = q.filter(db.Books.data.any(db.Data.format == extension))
for extension in exclude_extension_inputs: 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)) q = q.filter(db.Books.series.any(db.Series.id == serie))
for serie in exclude_series_inputs: for serie in exclude_series_inputs:
q = q.filter(not_(db.Books.series.any(db.Series.id == serie))) 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): 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))) .filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
if len(include_shelf_inputs) > 0: if len(include_shelf_inputs) > 0:
q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs)) q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
return q return q
def extend_search_term(searchterm, def extend_search_term(searchterm,
author_name, author_name,
book_title, book_title,
@ -1232,7 +1244,7 @@ def extend_search_term(searchterm,
format='medium', locale=get_locale())]) format='medium', locale=get_locale())])
except ValueError: except ValueError:
pub_end = u"" 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(): for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all() 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) 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, query = query.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))
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\ q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book) \
.outerjoin(db.Series)\ .outerjoin(db.Series) \
.filter(calibre_db.common_filters(True)) .filter(calibre_db.common_filters(True))
# parse multiselects to a complete dict # parse multiselects to a complete dict
@ -1311,43 +1323,43 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
if publisher: if publisher:
publisher = publisher.strip().lower() publisher = publisher.strip().lower()
searchterm = [] search_term = []
cc_present = False cc_present = False
for c in cc: for c in cc:
if c.datatype == "datetime": if c.datatype == "datetime":
column_start = term.get('custom_column_' + str(c.id) + '_start') column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end') column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start: if column_start:
searchterm.extend([u"{} >= {}".format(c.name, search_term.extend([u"{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(), format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium', format='medium',
locale=get_locale()) locale=get_locale())
)]) )])
cc_present = True cc_present = True
if column_end: if column_end:
searchterm.extend([u"{} <= {}".format(c.name, search_term.extend([u"{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(), format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium', format='medium',
locale=get_locale()) locale=get_locale())
)]) )])
cc_present = True cc_present = True
elif term.get('custom_column_' + str(c.id)): 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 cc_present = True
if any(tags.values()) or author_name or book_title or \
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \ publisher or pub_start or pub_end or rating_low or rating_high \
or rating_high or description or cc_present or read_status: or description or cc_present or read_status:
searchterm, pub_start, pub_end = extend_search_term(searchterm, search_term, pub_start, pub_end = extend_search_term(search_term,
author_name, author_name,
book_title, book_title,
publisher, publisher,
pub_start, pub_start,
pub_end, pub_end,
tags, tags,
rating_high, rating_high,
rating_low, rating_low,
read_status) read_status)
# q = q.filter() # q = q.filter()
if author_name: if author_name:
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + 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) q = adv_search_read_status(q, read_status)
if publisher: if publisher:
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + 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_text(q, tags['include_tag'], tags['exclude_tag'], db.Tags.id)
q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie']) 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_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_language(q, tags['include_language'], tags['exclude_language']) q = adv_search_ratings(q, rating_high, rating_low, )
q = adv_search_ratings(q, rating_high, rating_low)
if description: if description:
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + 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 limit_all = result_count
entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True) entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
return render_title_template('search.html', return render_title_template('search.html',
adv_searchterm=searchterm, adv_searchterm=search_term,
pagination=pagination, pagination=pagination,
entries=entries, entries=entries,
result_count=result_count, result_count=result_count,
@ -1414,10 +1426,12 @@ def advanced_search_form():
def get_cover(book_id): def get_cover(book_id):
return get_book_cover(book_id) return get_book_cover(book_id)
@web.route("/robots.txt") @web.route("/robots.txt")
def get_robots(): def get_robots():
return send_from_directory(constants.STATIC_DIR, "robots.txt") 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>", defaults={'anyname': 'None'})
@web.route("/show/<int:book_id>/<book_format>/<anyname>") @web.route("/show/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano @login_required_if_no_ano
@ -1561,7 +1575,7 @@ def login():
category="success") category="success")
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \ 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'))) login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session() ub.store_user_session()
log.info("Local Fallback Login as: '%s'", user.name) log.info("Local Fallback Login as: '%s'", user.name)
@ -1573,23 +1587,23 @@ def login():
log.info(error) log.info(error)
flash(_(u"Could not login: %(message)s", message=error), category="error") flash(_(u"Could not login: %(message)s", message=error), category="error")
else: else:
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr) 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) log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address)
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
else: 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 'forgot' in form and form['forgot'] == 'forgot':
if user is not None and user.name != "Guest": if user is not None and user.name != "Guest":
ret, __ = reset_password(user.id) ret, __ = reset_password(user.id)
if ret == 1: if ret == 1:
flash(_(u"New Password was send to your email address"), category="info") 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: else:
log.error(u"An unknown error occurred. Please try again later") log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_(u"An unknown error occurred. Please try again later."), category="error")
else: else:
flash(_(u"Please enter valid username to reset password"), category="error") 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: else:
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest": if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me'))) login_user(user, remember=bool(form.get('remember_me')))
@ -1599,7 +1613,7 @@ def login():
config.config_is_initial = False config.config_is_initial = False
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
else: 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") flash(_(u"Wrong Username or Password"), category="error")
next_url = request.args.get('next', default=url_for("web.index"), type=str) next_url = request.args.get('next', default=url_for("web.index"), type=str)
@ -1617,7 +1631,7 @@ def login():
@login_required @login_required
def logout(): def logout():
if current_user is not None and current_user.is_authenticated: 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() logout_user()
if feature_support['oauth'] and (config.config_login_type == 2 or config.config_login_type == 3): if feature_support['oauth'] and (config.config_login_type == 2 or config.config_login_type == 3):
logout_oauth_user() 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"]) current_user.email = check_email(to_save["email"])
if current_user.role_admin(): if current_user.role_admin():
if to_save.get("name", current_user.name) != current_user.name: 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.name = check_username(to_save["name"])
current_user.random_books = 1 if to_save.get("show_random") == "on" else 0 current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
if to_save.get("default_language"): if to_save.get("default_language"):
@ -1693,7 +1707,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
@login_required @login_required
def profile(): def profile():
languages = calibre_db.speaking_language() 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 kobo_support = feature_support['kobo'] and config.config_kobo_sync
if feature_support['oauth'] and config.config_login_type == 2: if feature_support['oauth'] and config.config_login_type == 2:
oauth_status = get_oauth_status() 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) book.ordered_authors = calibre_db.order_authors([book], False)
if not book: 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") log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
return redirect(url_for("web.index")) 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, return render_title_template('readcbr.html', comicfile=all_name, title=title,
extension=fileExt) extension=fileExt)
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible") 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")) return redirect(url_for("web.index"))
@ -1782,14 +1798,14 @@ def show_book(book_id):
entry = entries[0] entry = entries[0]
entry.read_status = read_book == ub.ReadBook.STATUS_FINISHED entry.read_status = read_book == ub.ReadBook.STATUS_FINISHED
entry.is_archived = archived_book entry.is_archived = archived_book
for index in range(0, len(entry.languages)): for lang_index in range(0, len(entry.languages)):
entry.languages[index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[ entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
index].lang_code) lang_index].lang_code)
cc = get_cc_columns(filter_config_custom_read=True) 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() shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
for sh in shelfs: 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) 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', return render_title_template('detail.html',
entry=entry, entry=entry,
cc=cc, cc=cc,
is_xhr=request.headers.get('X-Requested-With')=='XMLHttpRequest', is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
title=entry.title, title=entry.title,
books_shelfs=book_in_shelfs, books_shelfs=book_in_shelves,
page="book") page="book")
else: else:
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible") log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")

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

@ -12,6 +12,7 @@ SQLAlchemy>=1.3.0,<1.5.0
tornado>=4.1,<6.2 tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.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 flask-wtf>=0.14.2,<1.1.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0

Loading…
Cancel
Save