1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-24 18:47:23 +00:00

Refactored startup for compatibility with pyinstaller 5.0

This commit is contained in:
Ozzie Isaacs 2022-04-26 11:04:00 +02:00
parent db03fb3edd
commit 9410b47144
14 changed files with 283 additions and 230 deletions

77
cps.py
View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 OzzieIsaacs # Copyright (C) 2022 OzzieIsaacs
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -17,72 +17,19 @@
# 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 sys
import os import os
import sys
# Are we running from commandline?
if __package__ == '':
# Add local path to sys.path so we can import cps
path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, path)
# Insert local directories into path from cps.main import main as _main
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
from cps import create_app
from cps import web_server
from cps.opds import opds
from cps.web import web
from cps.jinjia import jinjia
from cps.about import about
from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import EditBook
from cps.remotelogin import remotelogin
from cps.search_metadata import meta
from cps.error_handler import init_errorhandler
from cps.schedule import register_scheduled_tasks, register_startup_tasks
try:
from cps.kobo import kobo, get_kobo_activated
from cps.kobo_auth import kobo_auth
kobo_available = get_kobo_activated()
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False
try:
from cps.oauth_bb import oauth
oauth_available = True
except ImportError:
oauth_available = False
def main():
app = create_app()
init_errorhandler()
app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive)
app.register_blueprint(EditBook)
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if oauth_available:
app.register_blueprint(oauth)
# Register scheduled tasks
register_scheduled_tasks() # ToDo only reconnect if reconnect is enabled
register_startup_tasks()
success = web_server.start()
sys.exit(0 if success else 1)
if __name__ == '__main__': if __name__ == '__main__':
main() _main()

View File

@ -19,8 +19,6 @@
# #
# 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/>.
__package__ = "cps"
import sys import sys
import os import os
import mimetypes import mimetypes
@ -28,15 +26,23 @@ import mimetypes
from babel import Locale as LC from babel import Locale as LC
from babel import negotiate_locale from babel import negotiate_locale
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask import Flask, request, g from flask import request, g
from flask import Flask
from .MyLoginManager import MyLoginManager from .MyLoginManager import MyLoginManager
from flask_babel import Babel from flask_babel import Babel
from flask_principal import Principal from flask_principal import Principal
from . import config_sql, logger, cache_buster, cli, ub, db from . import config_sql
from . import logger
from . import cache_buster
from .cli import CliParameter
from .constants import CONFIG_DIR
from . import ub, db
from .reverseproxy import ReverseProxied from .reverseproxy import ReverseProxied
from .server import WebServer from .server import WebServer
from .dep_check import dependency_check from .dep_check import dependency_check
from . import services
from .updater import Updater
try: try:
import lxml import lxml
@ -50,6 +56,7 @@ try:
except ImportError: except ImportError:
wtf_present = False wtf_present = False
mimetypes.init() mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('application/epub+zip', '.epub')
@ -81,37 +88,55 @@ app.config.update(
lm = MyLoginManager() lm = MyLoginManager()
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong'
if wtf_present:
csrf = CSRFProtect()
csrf.init_app(app)
else:
csrf = None
ub.init_db(cli.settings_path)
# pylint: disable=no-member
config = config_sql.load_configuration(ub.session)
web_server = WebServer()
babel = Babel() babel = Babel()
_BABEL_TRANSLATIONS = set() _BABEL_TRANSLATIONS = set()
log = logger.create() log = logger.create()
config = config_sql._ConfigSQL()
from . import services cli_param = CliParameter()
db.CalibreDB.update_config(config)
db.CalibreDB.setup_db(config.config_calibre_dir, cli.settings_path)
if wtf_present:
csrf = CSRFProtect()
else:
csrf = None
calibre_db = db.CalibreDB() calibre_db = db.CalibreDB()
web_server = WebServer()
updater_thread = Updater()
def create_app(): def create_app():
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong'
if csrf:
csrf.init_app(app)
cli_param.init()
ub.init_db(os.path.join(CONFIG_DIR, "app.db"), cli_param.user_credentials)
# ub.init_db(os.path.join(CONFIG_DIR, "app.db"))
# pylint: disable=no-member
config_sql.load_configuration(config, ub.session, cli_param)
db.CalibreDB.update_config(config)
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
calibre_db.init_db()
updater_thread.init_updater(config, web_server)
# Perform dry run of updater and exit afterwards
if cli_param.dry_run:
updater_thread.dry_run()
sys.exit(0)
updater_thread.start()
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
log.info( log.info(
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***') '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
@ -156,7 +181,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.LibraryId) config.store_calibre_uuid(calibre_db, db.Library_Id)
return app return app
@ -179,11 +204,8 @@ def get_locale():
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS) return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
from .updater import Updater '''@babel.timezoneselector
updater_thread = Updater() def get_timezone():
user = getattr(g, 'user', None)
return user.timezone if user else None'''
# Perform dry run of updater and exit afterwards
if cli.dry_run:
updater_thread.dry_run()
sys.exit(0)
updater_thread.start()

View File

@ -1254,7 +1254,7 @@ def _db_configuration_update_helper():
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.LibraryId) config.store_calibre_uuid(calibre_db, db.Library_Id)
# 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, all Calibre-Web info related to old Database gets deleted") log.info("Calibre Database changed, all Calibre-Web info related to old Database gets deleted")

View File

@ -31,96 +31,99 @@ def version_info():
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version'] return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1]) return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
class CliParameter(object):
parser = argparse.ArgumentParser(description='Calibre Web is a web app' def init(self):
' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py') self.arg_parser()
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path',
help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile')
parser.add_argument('-k', metavar='path',
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password and exits Calibre-Web')
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
parser.add_argument('-d', action='store_true', help='Dry run of updater to check file permissions in advance '
'and exits Calibre-Web')
parser.add_argument('-r', action='store_true', help='Enable public database reconnect route under /reconnect')
args = parser.parse_args()
settings_path = args.p or os.path.join(_CONFIG_DIR, DEFAULT_SETTINGS_FILE) def arg_parser(self):
gd_path = args.g or os.path.join(_CONFIG_DIR, DEFAULT_GDRIVE_FILE) parser = argparse.ArgumentParser(description='Calibre Web is a web app'
' providing a interface for browsing, reading and downloading eBooks\n',
prog='cps.py')
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path',
help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile')
parser.add_argument('-k', metavar='path',
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass',
help='Sets specific username to new password and exits Calibre-Web')
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
parser.add_argument('-d', action='store_true', help='Dry run of updater to check file permissions in advance '
'and exits Calibre-Web')
parser.add_argument('-r', action='store_true', help='Enable public database reconnect route under /reconnect')
args = parser.parse_args()
if os.path.isdir(settings_path): self.settings_path = args.p or os.path.join(_CONFIG_DIR, DEFAULT_SETTINGS_FILE)
settings_path = os.path.join(settings_path, DEFAULT_SETTINGS_FILE) self.gd_path = args.g or os.path.join(_CONFIG_DIR, DEFAULT_GDRIVE_FILE)
if os.path.isdir(gd_path): if os.path.isdir(self.settings_path):
gd_path = os.path.join(gd_path, DEFAULT_GDRIVE_FILE) self.settings_path = os.path.join(self.settings_path, DEFAULT_SETTINGS_FILE)
if os.path.isdir(self.gd_path):
self.gd_path = os.path.join(self.gd_path, DEFAULT_GDRIVE_FILE)
# handle and check parameter for ssl encryption # handle and check parameter for ssl encryption
certfilepath = None self.certfilepath = None
keyfilepath = None self.keyfilepath = None
if args.c: if args.c:
if os.path.isfile(args.c): if os.path.isfile(args.c):
certfilepath = args.c self.certfilepath = args.c
else:
print("Certfile path is invalid. Exiting...")
sys.exit(1)
if args.c == "":
certfilepath = ""
if args.k:
if os.path.isfile(args.k):
keyfilepath = args.k
else:
print("Keyfile path is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
print("Certfile and Keyfile have to be used together. Exiting...")
sys.exit(1)
if args.k == "":
keyfilepath = ""
# dry run updater
dry_run = args.d or None
# enable reconnect endpoint for docker database reconnect
reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
# load covers from localhost
allow_localhost = args.l or os.environ.get("CALIBRE_LOCALHOST", None)
# handle and check ip address argument
ip_address = args.i or None
if ip_address:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in ip_address:
socket.inet_pton(socket.AF_INET6, ip_address)
else: else:
socket.inet_pton(socket.AF_INET, ip_address) print("Certfile path is invalid. Exiting...")
else: sys.exit(1)
# on windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(ip_address)
except socket.error as err:
print(ip_address, ':', err)
sys.exit(1)
# handle and check user password argument if args.c == "":
user_credentials = args.s or None self.certfilepath = ""
if user_credentials and ":" not in user_credentials:
print("No valid 'username:password' format")
sys.exit(3)
if args.f: if args.k:
print("Warning: -f flag is depreciated and will be removed in next version") if os.path.isfile(args.k):
self.keyfilepath = args.k
else:
print("Keyfile path is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
print("Certfile and Keyfile have to be used together. Exiting...")
sys.exit(1)
if args.k == "":
self.keyfilepath = ""
# dry run updater
self.dry_run =args.d or None
# enable reconnect endpoint for docker database reconnect
self.reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
# load covers from localhost
self.allow_localhost = args.l or os.environ.get("CALIBRE_LOCALHOST", None)
# handle and check ip address argument
self.ip_address = args.i or None
if self.ip_address:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in self.ip_address:
socket.inet_pton(socket.AF_INET6, self.ip_address)
else:
socket.inet_pton(socket.AF_INET, self.ip_address)
else:
# on windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(self.ip_address)
except socket.error as err:
print(self.ip_address, ':', err)
sys.exit(1)
# handle and check user password argument
self.user_credentials = args.s or None
if self.user_credentials and ":" not in self.user_credentials:
print("No valid 'username:password' format")
sys.exit(3)
if args.f:
print("Warning: -f flag is depreciated and will be removed in next version")

View File

@ -29,7 +29,7 @@ try:
except ImportError: except ImportError:
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger from . import constants, logger
log = logger.create() log = logger.create()
@ -154,12 +154,16 @@ class _Settings(_Base):
# Class holds all application specific settings in calibre-web # Class holds all application specific settings in calibre-web
class _ConfigSQL(object): class _ConfigSQL(object):
# pylint: disable=no-member # pylint: disable=no-member
def __init__(self, session): def __init__(self):
pass
def init_config(self, session, cli):
self._session = session self._session = session
self._settings = None self._settings = None
self.db_configured = None self.db_configured = None
self.config_calibre_dir = None self.config_calibre_dir = None
self.load() self.load()
self.cli = cli
change = False change = False
if self.config_converterpath == None: # pylint: disable=access-member-before-definition if self.config_converterpath == None: # pylint: disable=access-member-before-definition
@ -184,22 +188,21 @@ class _ConfigSQL(object):
return self._settings return self._settings
def get_config_certfile(self): def get_config_certfile(self):
if cli.certfilepath: if self.cli.certfilepath:
return cli.certfilepath return self.cli.certfilepath
if cli.certfilepath == "": if self.cli.certfilepath == "":
return None return None
return self.config_certfile return self.config_certfile
def get_config_keyfile(self): def get_config_keyfile(self):
if cli.keyfilepath: if self.cli.keyfilepath:
return cli.keyfilepath return self.cli.keyfilepath
if cli.certfilepath == "": if self.cli.certfilepath == "":
return None return None
return self.config_keyfile return self.config_keyfile
@staticmethod def get_config_ipaddress(self):
def get_config_ipaddress(): return self.cli.ip_address or ""
return cli.ip_address or ""
def _has_role(self, role_flag): def _has_role(self, role_flag):
return constants.has_flag(self.config_default_role, role_flag) return constants.has_flag(self.config_default_role, role_flag)
@ -449,14 +452,15 @@ def _migrate_database(session):
_migrate_table(session, _Flask_Settings) _migrate_table(session, _Flask_Settings)
def load_configuration(session): def load_configuration(conf, session, cli):
_migrate_database(session) _migrate_database(session)
if not session.query(_Settings).count(): if not session.query(_Settings).count():
session.add(_Settings()) session.add(_Settings())
session.commit() session.commit()
conf = _ConfigSQL(session) # conf = _ConfigSQL()
return conf conf.init_config(session, cli)
# return conf
def get_flask_session_key(_session): def get_flask_session_key(_session):
flask_settings = _session.query(_Flask_Settings).one_or_none() flask_settings = _session.query(_Flask_Settings).one_or_none()

View File

@ -89,7 +89,7 @@ books_publishers_link = Table('books_publishers_link', Base.metadata,
) )
class LibraryId(Base): class Library_Id(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)
@ -440,10 +440,12 @@ class CalibreDB:
# instances alive once they reach the end of their respective scopes # instances alive once they reach the end of their respective scopes
instances = WeakSet() instances = WeakSet()
def __init__(self, expire_on_commit=True): def __init__(self):
""" Initialize a new CalibreDB session """ Initialize a new CalibreDB session
""" """
self.session = None self.session = None
def init_db(self, expire_on_commit=True):
if self._init: if self._init:
self.init_session(expire_on_commit) self.init_session(expire_on_commit)
@ -543,7 +545,7 @@ class CalibreDB:
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(LibraryId).one_or_none() database_uuid = local_session().query(Library_Id).one_or_none()
# local_session.dispose() # local_session.dispose()
check_engine.connect() check_engine.connect()

View File

@ -49,7 +49,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('edit-book', __name__) editbook = Blueprint('edit-book', __name__)
log = logger.create() log = logger.create()
@ -228,14 +228,14 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
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)
@ -743,14 +743,14 @@ 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']) @editbook.route("/admin/book/<int:book_id>", methods=['GET'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
def show_edit_book(book_id): def show_edit_book(book_id):
return render_edit_book(book_id) return render_edit_book(book_id)
@EditBook.route("/admin/book/<int:book_id>", methods=['POST']) @editbook.route("/admin/book/<int:book_id>", methods=['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):
@ -1084,7 +1084,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():
@ -1153,7 +1153,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):
@ -1178,7 +1178,7 @@ def convert_bookformat(book_id):
return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_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()
@ -1191,7 +1191,7 @@ def table_get_custom_enum(c_id):
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):
@ -1303,7 +1303,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']:
@ -1320,7 +1320,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():
@ -1336,7 +1336,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():
@ -1374,7 +1374,7 @@ def merge_list_book():
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():

View File

@ -63,7 +63,7 @@ except ImportError as err:
importError = err importError = err
gdrive_support = False gdrive_support = False
from . import logger, cli, config from . import logger, cli_param, config
from .constants import CONFIG_DIR as _CONFIG_DIR from .constants import CONFIG_DIR as _CONFIG_DIR
@ -142,7 +142,7 @@ def is_gdrive_ready():
return os.path.exists(SETTINGS_YAML) and os.path.exists(CREDENTIALS) return os.path.exists(SETTINGS_YAML) and os.path.exists(CREDENTIALS)
engine = create_engine('sqlite:///{0}'.format(cli.gd_path), echo=False) engine = create_engine('sqlite:///{0}'.format(cli_param.gd_path), echo=False)
Base = declarative_base() Base = declarative_base()
# Open session for database connection # Open session for database connection
@ -190,11 +190,11 @@ def migrate():
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids') session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
break break
if not os.path.exists(cli.gd_path): if not os.path.exists(cli_param.gd_path):
try: try:
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
except Exception as ex: except Exception as ex:
log.error("Error connect to database: {} - {}".format(cli.gd_path, ex)) log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
raise raise
migrate() migrate()
@ -544,6 +544,7 @@ def deleteDatabaseOnChange():
except (OperationalError, InvalidRequestError) as ex: except (OperationalError, InvalidRequestError) as ex:
session.rollback() session.rollback()
log.error_or_exception('Database error: {}'.format(ex)) log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
def updateGdriveCalibreFromLocal(): def updateGdriveCalibreFromLocal():

71
cps/main.py Normal file
View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2022 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from . import create_app
from .jinjia import jinjia
from .shelf import shelf
from .remotelogin import remotelogin
from .search_metadata import meta
from .error_handler import init_errorhandler
try:
from kobo import kobo, get_kobo_activated
from kobo_auth import kobo_auth
kobo_available = get_kobo_activated()
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False
try:
from oauth_bb import oauth
oauth_available = True
except ImportError:
oauth_available = False
def main():
app = create_app()
from .web import web
from .opds import opds
from .admin import admi
from .gdrive import gdrive
from .editbooks import editbook
from .about import about
from . import web_server
init_errorhandler()
app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi) #
app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
if kobo_available:
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if oauth_available:
app.register_blueprint(oauth)
success = web_server.start()
sys.exit(0 if success else 1)

View File

@ -24,11 +24,10 @@ import mimetypes
from io import StringIO from io import StringIO
from email.message import EmailMessage from email.message import EmailMessage
from email.utils import parseaddr from email.utils import formatdate, parseaddr
from email.generator import Generator
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from email.utils import formatdate from email.utils import formatdate
from email.generator import Generator
from cps.services.worker import CalibreTask from cps.services.worker import CalibreTask
from cps.services import gmail from cps.services import gmail

View File

@ -210,7 +210,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2>{{_('Update')}}</h2> <h2>{{_('Version Information')}}</h2>
<table class="table table-striped" id="update_table"> <table class="table table-striped" id="update_table">
<thead> <thead>
<tr> <tr>

View File

@ -53,7 +53,7 @@ except ImportError:
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from . import constants, logger, cli from . import constants, logger
log = logger.create() log = logger.create()
@ -816,7 +816,7 @@ def init_db_thread():
return Session() return Session()
def init_db(app_db_path): def init_db(app_db_path, user_credentials=None):
# Open session for database connection # Open session for database connection
global session global session
global app_DB_path global app_DB_path
@ -837,8 +837,8 @@ def init_db(app_db_path):
create_admin_user(session) create_admin_user(session)
create_anonymous_user(session) create_anonymous_user(session)
if cli.user_credentials: if user_credentials:
username, password = cli.user_credentials.split(':', 1) username, password = user_credentials.split(':', 1)
user = session.query(User).filter(func.lower(User.name) == username.lower()).first() user = session.query(User).filter(func.lower(User.name) == username.lower()).first()
if user: if user:
if not password: if not password:

View File

@ -31,7 +31,7 @@ import requests
from babel.dates import format_datetime from babel.dates import format_datetime
from flask_babel import gettext as _ from flask_babel import gettext as _
from . import constants, logger, config, web_server from . import constants, logger # config, web_server
log = logger.create() log = logger.create()
@ -58,13 +58,17 @@ class Updater(threading.Thread):
self.status = -1 self.status = -1
self.updateIndex = None self.updateIndex = None
def init_updater(self, config, web_server):
self.config = config
self.web_server = web_server
def get_current_version_info(self): def get_current_version_info(self):
if config.config_updatechannel == constants.UPDATE_STABLE: if self.config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_version_info() return self._stable_version_info()
return self._nightly_version_info() return self._nightly_version_info()
def get_available_updates(self, request_method, locale): def get_available_updates(self, request_method, locale):
if config.config_updatechannel == constants.UPDATE_STABLE: if self.config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_available_updates(request_method) return self._stable_available_updates(request_method)
return self._nightly_available_updates(request_method, locale) return self._nightly_available_updates(request_method, locale)
@ -95,7 +99,7 @@ class Updater(threading.Thread):
self.status = 6 self.status = 6
log.debug(u'Preparing restart of server') log.debug(u'Preparing restart of server')
time.sleep(2) time.sleep(2)
web_server.stop(True) self.web_server.stop(True)
self.status = 7 self.status = 7
time.sleep(2) time.sleep(2)
return True return True

View File

@ -54,7 +54,7 @@ install_requires =
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 advocate>=1.0.0,<1.1.0
@ -62,7 +62,7 @@ install_requires =
[options.extras_require] [options.extras_require]
gdrive = gdrive =
google-api-python-client>=1.7.11,<2.44.0 google-api-python-client>=1.7.11,<2.46.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
@ -75,7 +75,7 @@ gdrive =
rsa>=3.4.2,<4.9.0 rsa>=3.4.2,<4.9.0
gmail = gmail =
google-auth-oauthlib>=0.4.3,<0.6.0 google-auth-oauthlib>=0.4.3,<0.6.0
google-api-python-client>=1.7.11,<2.44.0 google-api-python-client>=1.7.11,<2.46.0
goodreads = goodreads =
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0 python-Levenshtein>=0.12.0,<0.13.0