diff --git a/cps.py b/cps.py index d38a9f33..e216e149 100755 --- a/cps.py +++ b/cps.py @@ -1,14 +1,53 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2012-2019 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 . + +import os import sys + +base_path = os.path.dirname(os.path.abspath(__file__)) +# Insert local directories into path +sys.path.append(base_path) +sys.path.append(os.path.join(base_path, 'cps')) +sys.path.append(os.path.join(base_path, 'vendor')) + from cps import create_app -from cps.web import web +from cps.opds import opds from cps import Server +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 + if __name__ == '__main__': app = create_app() 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(gdrive) + app.register_blueprint(editbook) Server.startServer() diff --git a/cps/__init__.py b/cps/__init__.py index 1170a85a..27be8451 100755 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -4,32 +4,56 @@ # import logging # from logging.handlers import SMTPHandler, RotatingFileHandler # import os - -from flask import Flask# , request, current_app +import mimetypes +from flask import Flask, request, g from flask_login import LoginManager -from flask_babel import Babel # , lazy_gettext as _l +from flask_babel import Babel import cache_buster from reverseproxy import ReverseProxied import logging from logging.handlers import RotatingFileHandler from flask_principal import Principal -# from flask_sqlalchemy import SQLAlchemy +from babel.core import UnknownLocaleError +from babel import Locale as LC +from babel import negotiate_locale import os import ub from ub import Config, Settings -import cPickle +try: + import cPickle +except ImportError: + import pickle as cPickle -# Normal -babel = Babel() + + + +mimetypes.init() +mimetypes.add_type('application/xhtml+xml', '.xhtml') +mimetypes.add_type('application/epub+zip', '.epub') +mimetypes.add_type('application/fb2+zip', '.fb2') +mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') +mimetypes.add_type('application/x-mobipocket-ebook', '.prc') +mimetypes.add_type('application/vnd.amazon.ebook', '.azw') +mimetypes.add_type('application/x-cbr', '.cbr') +mimetypes.add_type('application/x-cbz', '.cbz') +mimetypes.add_type('application/x-cbt', '.cbt') +mimetypes.add_type('image/vnd.djvu', '.djvu') +mimetypes.add_type('application/mpeg', '.mpeg') +mimetypes.add_type('application/mpeg', '.mp3') +mimetypes.add_type('application/mp4', '.m4a') +mimetypes.add_type('application/mp4', '.m4b') +mimetypes.add_type('application/ogg', '.ogg') +mimetypes.add_type('application/ogg', '.oga') + +app = Flask(__name__) + lm = LoginManager() lm.login_view = 'web.login' lm.anonymous_user = ub.Anonymous - -ub_session = ub.session -# ub_session.start() +ub.init_db() config = Config() @@ -42,15 +66,14 @@ searched_ids = {} from worker import WorkerThread - global_WorkerThread = WorkerThread() from server import server Server = server() +babel = Babel() def create_app(): - app = Flask(__name__) app.wsgi_app = ReverseProxied(app.wsgi_app) cache_buster.init_cache_busting(app) @@ -71,15 +94,38 @@ def create_app(): logging.getLogger("book_formats").setLevel(config.config_log_level) Principal(app) lm.init_app(app) - babel.init_app(app) app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT') Server.init_app(app) db.setup_db() + babel.init_app(app) global_WorkerThread.start() - # app.config.from_object(config_class) - # db.init_app(app) - # login.init_app(app) - - return app + +@babel.localeselector +def get_locale(): + # if a user is logged in, use the locale from the user settings + user = getattr(g, 'user', None) + # user = None + if user is not None and hasattr(user, "locale"): + if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings + return user.locale + translations = [str(item) for item in babel.list_translations()] + ['en'] + preferred = list() + for x in request.accept_languages.values(): + try: + preferred.append(str(LC.parse(x.replace('-', '_')))) + except (UnknownLocaleError, ValueError) as e: + app.logger.debug("Could not parse locale: %s", e) + preferred.append('en') + return negotiate_locale(preferred, translations) + + +@babel.timezoneselector +def get_timezone(): + user = getattr(g, 'user', None) + if user is not None: + return user.timezone + +from updater import Updater +updater_thread = Updater() diff --git a/cps/about.py b/cps/about.py new file mode 100644 index 00000000..6e82b964 --- /dev/null +++ b/cps/about.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from flask import Blueprint +from flask_login import login_required +import db +import sys +import uploader +from babel import __version__ as babelVersion +from sqlalchemy import __version__ as sqlalchemyVersion +from flask_principal import __version__ as flask_principalVersion +from iso639 import __version__ as iso639Version +from pytz import __version__ as pytzVersion +from flask import __version__ as flaskVersion +from werkzeug import __version__ as werkzeugVersion +from jinja2 import __version__ as jinja2Version +import converter +from flask_babel import gettext as _ +from cps import Server +import requests +from web import render_title_template + +try: + from flask_login import __version__ as flask_loginVersion +except ImportError: + from flask_login.__about__ import __version__ as flask_loginVersion + +about = Blueprint('about', __name__) + + +@about.route("/stats") +@login_required +def stats(): + counter = db.session.query(db.Books).count() + authors = db.session.query(db.Authors).count() + categorys = db.session.query(db.Tags).count() + series = db.session.query(db.Series).count() + versions = uploader.get_versions() + versions['Babel'] = 'v' + babelVersion + versions['Sqlalchemy'] = 'v' + sqlalchemyVersion + versions['Werkzeug'] = 'v' + werkzeugVersion + versions['Jinja2'] = 'v' + jinja2Version + versions['Flask'] = 'v' + flaskVersion + versions['Flask Login'] = 'v' + flask_loginVersion + versions['Flask Principal'] = 'v' + flask_principalVersion + versions['Iso 639'] = 'v' + iso639Version + versions['pytz'] = 'v' + pytzVersion + + versions['Requests'] = 'v' + requests.__version__ + versions['pySqlite'] = 'v' + db.engine.dialect.dbapi.version + versions['Sqlite'] = 'v' + db.engine.dialect.dbapi.sqlite_version + versions.update(converter.versioncheck()) + versions.update(Server.getNameVersion()) + versions['Python'] = sys.version + return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, + categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat") diff --git a/cps/admin.py b/cps/admin.py new file mode 100644 index 00000000..cd6ff60c --- /dev/null +++ b/cps/admin.py @@ -0,0 +1,776 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +from flask import Blueprint +from flask import abort, request +from flask_login import login_required, current_user +from web import admin_required, render_title_template, flash, redirect, url_for, before_request, logout_user, \ + speaking_language, unconfigured +from cps import db, ub, Server, get_locale, config, app, updater_thread, babel +import json +from datetime import datetime, timedelta +import time +from babel.dates import format_datetime +from flask_babel import gettext as _ +from babel import Locale as LC +from sqlalchemy.exc import IntegrityError +from gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDatabaseOnChange, listRootFolders +from web import login_required_if_no_ano, check_valid_domain +import helper +from werkzeug.security import generate_password_hash + +try: + from goodreads.client import GoodreadsClient + goodreads_support = True +except ImportError: + goodreads_support = False + +try: + import rarfile + rar_support = True +except ImportError: + rar_support = False + + +admi = Blueprint('admin', __name__) + + +@admi.route("/admin") +@login_required +def admin_forbidden(): + abort(403) + + +@admi.route("/shutdown") +@login_required +@admin_required +def shutdown(): + task = int(request.args.get("parameter").strip()) + if task == 1 or task == 0: # valid commandos received + # close all database connections + db.session.close() + db.engine.dispose() + ub.session.close() + ub.engine.dispose() + + showtext = {} + if task == 0: + showtext['text'] = _(u'Server restarted, please reload page') + Server.setRestartTyp(True) + else: + showtext['text'] = _(u'Performing shutdown of server, please close window') + Server.setRestartTyp(False) + # stop gevent/tornado server + Server.stopServer() + return json.dumps(showtext) + else: + if task == 2: + db.session.close() + db.engine.dispose() + db.setup_db() + return json.dumps({}) + abort(404) + + +@admi.route("/admin/view") +@login_required +@admin_required +def admin(): + version = updater_thread.get_current_version_info() + if version is False: + commit = _(u'Unknown') + else: + if 'datetime' in version: + commit = version['datetime'] + + tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) + form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S") + if len(commit) > 19: # check if string has timezone + if commit[19] == '+': + form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) + elif commit[19] == '-': + form_date += timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) + commit = format_datetime(form_date - tz, format='short', locale=get_locale()) + else: + commit = version['version'] + + content = ub.session.query(ub.User).all() + settings = ub.session.query(ub.Settings).first() + return render_title_template("admin.html", content=content, email=settings, config=config, commit=commit, + title=_(u"Admin page"), page="admin") + + +@admi.route("/admin/config", methods=["GET", "POST"]) +@login_required +@admin_required +def configuration(): + return configuration_helper(0) + + +@admi.route("/admin/viewconfig", methods=["GET", "POST"]) +@login_required +@admin_required +def view_configuration(): + reboot_required = False + if request.method == "POST": + to_save = request.form.to_dict() + content = ub.session.query(ub.Settings).first() + if "config_calibre_web_title" in to_save: + content.config_calibre_web_title = to_save["config_calibre_web_title"] + if "config_columns_to_ignore" in to_save: + content.config_columns_to_ignore = to_save["config_columns_to_ignore"] + if "config_read_column" in to_save: + content.config_read_column = int(to_save["config_read_column"]) + if "config_theme" in to_save: + content.config_theme = int(to_save["config_theme"]) + if "config_title_regex" in to_save: + if content.config_title_regex != to_save["config_title_regex"]: + content.config_title_regex = to_save["config_title_regex"] + reboot_required = True + if "config_random_books" in to_save: + content.config_random_books = int(to_save["config_random_books"]) + if "config_books_per_page" in to_save: + content.config_books_per_page = int(to_save["config_books_per_page"]) + # Mature Content configuration + if "config_mature_content_tags" in to_save: + content.config_mature_content_tags = to_save["config_mature_content_tags"].strip() + + # Default user configuration + content.config_default_role = 0 + if "admin_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_ADMIN + if "download_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_DOWNLOAD + if "upload_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_UPLOAD + if "edit_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_EDIT + if "delete_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_DELETE_BOOKS + if "passwd_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_PASSWD + if "edit_shelf_role" in to_save: + content.config_default_role = content.config_default_role + ub.ROLE_EDIT_SHELFS + + content.config_default_show = 0 + if "show_detail_random" in to_save: + content.config_default_show = content.config_default_show + ub.DETAIL_RANDOM + if "show_language" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_LANGUAGE + if "show_series" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_SERIES + if "show_category" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_CATEGORY + if "show_hot" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_HOT + if "show_random" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_RANDOM + if "show_author" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_AUTHOR + if "show_publisher" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_PUBLISHER + if "show_best_rated" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_BEST_RATED + if "show_read_and_unread" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_READ_AND_UNREAD + if "show_recent" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_RECENT + if "show_sorted" in to_save: + content.config_default_show = content.config_default_show + ub.SIDEBAR_SORTED + if "show_mature_content" in to_save: + content.config_default_show = content.config_default_show + ub.MATURE_CONTENT + ub.session.commit() + flash(_(u"Calibre-Web configuration updated"), category="success") + config.loadSettings() + before_request() + if reboot_required: + # db.engine.dispose() # ToDo verify correct + # ub.session.close() + # ub.engine.dispose() + # stop Server + Server.setRestartTyp(True) + Server.stopServer() + app.logger.info('Reboot required, restarting') + readColumn = db.session.query(db.Custom_Columns)\ + .filter(db.and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() + return render_title_template("config_view_edit.html", content=config, readColumns=readColumn, + title=_(u"UI Configuration"), page="uiconfig") + + +@admi.route("/config", methods=["GET", "POST"]) +@unconfigured +def basic_configuration(): + logout_user() + return configuration_helper(1) + + +def configuration_helper(origin): + reboot_required = False + gdriveError = None + db_change = False + success = False + filedata = None + if gdrive_support is False: + gdriveError = _('Import of optional Google Drive requirements missing') + else: + if not os.path.isfile(os.path.join(config.get_main_dir, 'client_secrets.json')): + gdriveError = _('client_secrets.json is missing or not readable') + else: + with open(os.path.join(config.get_main_dir, 'client_secrets.json'), 'r') as settings: + filedata = json.load(settings) + if 'web' not in filedata: + gdriveError = _('client_secrets.json is not configured for web application') + if request.method == "POST": + to_save = request.form.to_dict() + content = ub.session.query(ub.Settings).first() # type: ub.Settings + if "config_calibre_dir" in to_save: + if content.config_calibre_dir != to_save["config_calibre_dir"]: + content.config_calibre_dir = to_save["config_calibre_dir"] + db_change = True + # Google drive setup + if not os.path.isfile(os.path.join(config.get_main_dir, 'settings.yaml')): + content.config_use_google_drive = False + if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError: + if filedata: + if filedata['web']['redirect_uris'][0].endswith('/'): + filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-1] + with open(os.path.join(config.get_main_dir, 'settings.yaml'), 'w') as f: + yaml = "client_config_backend: settings\nclient_config_file: %(client_file)s\n" \ + "client_config:\n" \ + " client_id: %(client_id)s\n client_secret: %(client_secret)s\n" \ + " redirect_uri: %(redirect_uri)s\n\nsave_credentials: True\n" \ + "save_credentials_backend: file\nsave_credentials_file: %(credential)s\n\n" \ + "get_refresh_token: True\n\noauth_scope:\n" \ + " - https://www.googleapis.com/auth/drive\n" + f.write(yaml % {'client_file': os.path.join(config.get_main_dir, 'client_secrets.json'), + 'client_id': filedata['web']['client_id'], + 'client_secret': filedata['web']['client_secret'], + 'redirect_uri': filedata['web']['redirect_uris'][0], + 'credential': os.path.join(config.get_main_dir, 'gdrive_credentials')}) + else: + flash(_(u'client_secrets.json is not configured for web application'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + # always show google drive settings, but in case of error deny support + if "config_use_google_drive" in to_save and not gdriveError: + content.config_use_google_drive = "config_use_google_drive" in to_save + else: + content.config_use_google_drive = 0 + if "config_google_drive_folder" in to_save: + if content.config_google_drive_folder != to_save["config_google_drive_folder"]: + content.config_google_drive_folder = to_save["config_google_drive_folder"] + deleteDatabaseOnChange() + + if "config_port" in to_save: + if content.config_port != int(to_save["config_port"]): + content.config_port = int(to_save["config_port"]) + reboot_required = True + if "config_keyfile" in to_save: + if content.config_keyfile != to_save["config_keyfile"]: + if os.path.isfile(to_save["config_keyfile"]) or to_save["config_keyfile"] is u"": + content.config_keyfile = to_save["config_keyfile"] + reboot_required = True + else: + ub.session.commit() + flash(_(u'Keyfile location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + if "config_certfile" in to_save: + if content.config_certfile != to_save["config_certfile"]: + if os.path.isfile(to_save["config_certfile"]) or to_save["config_certfile"] is u"": + content.config_certfile = to_save["config_certfile"] + reboot_required = True + else: + ub.session.commit() + flash(_(u'Certfile location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + content.config_uploading = 0 + content.config_anonbrowse = 0 + content.config_public_reg = 0 + if "config_uploading" in to_save and to_save["config_uploading"] == "on": + content.config_uploading = 1 + if "config_anonbrowse" in to_save and to_save["config_anonbrowse"] == "on": + content.config_anonbrowse = 1 + if "config_public_reg" in to_save and to_save["config_public_reg"] == "on": + content.config_public_reg = 1 + + if "config_converterpath" in to_save: + content.config_converterpath = to_save["config_converterpath"].strip() + if "config_calibre" in to_save: + content.config_calibre = to_save["config_calibre"].strip() + if "config_ebookconverter" in to_save: + content.config_ebookconverter = int(to_save["config_ebookconverter"]) + + #LDAP configurator, + if "config_use_ldap" in to_save and to_save["config_use_ldap"] == "on": + if "config_ldap_provider_url" not in to_save or "config_ldap_dn" not in to_save: + ub.session.commit() + flash(_(u'Please enter a LDAP provider and a DN'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + else: + content.config_use_ldap = 1 + content.config_ldap_provider_url = to_save["config_ldap_provider_url"] + content.config_ldap_dn = to_save["config_ldap_dn"] + db_change = True + + # Remote login configuration + content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on") + if not content.config_remote_login: + ub.session.query(ub.RemoteAuthToken).delete() + + # Goodreads configuration + content.config_use_goodreads = ("config_use_goodreads" in to_save and to_save["config_use_goodreads"] == "on") + if "config_goodreads_api_key" in to_save: + content.config_goodreads_api_key = to_save["config_goodreads_api_key"] + if "config_goodreads_api_secret" in to_save: + content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] + if "config_updater" in to_save: + content.config_updatechannel = int(to_save["config_updater"]) + + # GitHub OAuth configuration + content.config_use_github_oauth = ("config_use_github_oauth" in to_save and + to_save["config_use_github_oauth"] == "on") + if "config_github_oauth_client_id" in to_save: + content.config_github_oauth_client_id = to_save["config_github_oauth_client_id"] + if "config_github_oauth_client_secret" in to_save: + content.config_github_oauth_client_secret = to_save["config_github_oauth_client_secret"] + + if content.config_github_oauth_client_id != config.config_github_oauth_client_id or \ + content.config_github_oauth_client_secret != config.config_github_oauth_client_secret: + reboot_required = True + + # Google OAuth configuration + content.config_use_google_oauth = ("config_use_google_oauth" in to_save and + to_save["config_use_google_oauth"] == "on") + if "config_google_oauth_client_id" in to_save: + content.config_google_oauth_client_id = to_save["config_google_oauth_client_id"] + if "config_google_oauth_client_secret" in to_save: + content.config_google_oauth_client_secret = to_save["config_google_oauth_client_secret"] + + if content.config_google_oauth_client_id != config.config_google_oauth_client_id or \ + content.config_google_oauth_client_secret != config.config_google_oauth_client_secret: + reboot_required = True + + if "config_log_level" in to_save: + content.config_log_level = int(to_save["config_log_level"]) + if content.config_logfile != to_save["config_logfile"]: + # check valid path, only path or file + if os.path.dirname(to_save["config_logfile"]): + if os.path.exists(os.path.dirname(to_save["config_logfile"])) and \ + os.path.basename(to_save["config_logfile"]) and not os.path.isdir(to_save["config_logfile"]): + content.config_logfile = to_save["config_logfile"] + else: + ub.session.commit() + flash(_(u'Logfile location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + else: + content.config_logfile = to_save["config_logfile"] + reboot_required = True + + # Rarfile Content configuration + if "config_rarfile_location" in to_save and to_save['config_rarfile_location'] is not u"": + check = helper.check_unrar(to_save["config_rarfile_location"].strip()) + if not check[0] : + content.config_rarfile_location = to_save["config_rarfile_location"].strip() + else: + flash(check[1], category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, goodreads=goodreads_support, + rarfile_support=rar_support, title=_(u"Basic Configuration")) + try: + if content.config_use_google_drive and is_gdrive_ready() and not \ + os.path.exists(os.path.join(content.config_calibre_dir, "metadata.db")): + downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db") + if db_change: + if config.db_configured: + db.session.close() + db.engine.dispose() + ub.session.commit() + flash(_(u"Calibre-Web configuration updated"), category="success") + config.loadSettings() + app.logger.setLevel(config.config_log_level) + logging.getLogger("book_formats").setLevel(config.config_log_level) + except Exception as e: + flash(e, category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, rarfile_support=rar_support, + title=_(u"Basic Configuration"), page="config") + if db_change: + reload(db) + if not db.setup_db(): + flash(_(u'DB location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, rarfile_support=rar_support, + title=_(u"Basic Configuration"), page="config") + if reboot_required: + # stop Server + Server.setRestartTyp(True) + Server.stopServer() + app.logger.info('Reboot required, restarting') + if origin: + success = True + if is_gdrive_ready() and gdrive_support is True: # and config.config_use_google_drive == True: + gdrivefolders = listRootFolders() + else: + gdrivefolders = list() + return render_title_template("config_edit.html", origin=origin, success=success, content=config, + show_authenticate_google_drive=not is_gdrive_ready(), + gdrive=gdrive_support, gdriveError=gdriveError, + gdrivefolders=gdrivefolders, rarfile_support=rar_support, + goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") + + +@admi.route("/admin/user/new", methods=["GET", "POST"]) +@login_required +@admin_required +def new_user(): + content = ub.User() + languages = speaking_language() + translations = [LC('en')] + babel.list_translations() + if request.method == "POST": + to_save = request.form.to_dict() + content.default_language = to_save["default_language"] + content.mature_content = "show_mature_content" in to_save + if "locale" in to_save: + content.locale = to_save["locale"] + content.sidebar_view = 0 + if "show_random" in to_save: + content.sidebar_view += ub.SIDEBAR_RANDOM + if "show_language" in to_save: + content.sidebar_view += ub.SIDEBAR_LANGUAGE + if "show_series" in to_save: + content.sidebar_view += ub.SIDEBAR_SERIES + if "show_category" in to_save: + content.sidebar_view += ub.SIDEBAR_CATEGORY + if "show_hot" in to_save: + content.sidebar_view += ub.SIDEBAR_HOT + if "show_read_and_unread" in to_save: + content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD + if "show_best_rated" in to_save: + content.sidebar_view += ub.SIDEBAR_BEST_RATED + if "show_author" in to_save: + content.sidebar_view += ub.SIDEBAR_AUTHOR + if "show_publisher" in to_save: + content.sidebar_view += ub.SIDEBAR_PUBLISHER + if "show_detail_random" in to_save: + content.sidebar_view += ub.DETAIL_RANDOM + if "show_sorted" in to_save: + content.sidebar_view += ub.SIDEBAR_SORTED + if "show_recent" in to_save: + content.sidebar_view += ub.SIDEBAR_RECENT + + content.role = 0 + if "admin_role" in to_save: + content.role = content.role + ub.ROLE_ADMIN + if "download_role" in to_save: + content.role = content.role + ub.ROLE_DOWNLOAD + if "upload_role" in to_save: + content.role = content.role + ub.ROLE_UPLOAD + if "edit_role" in to_save: + content.role = content.role + ub.ROLE_EDIT + if "delete_role" in to_save: + content.role = content.role + ub.ROLE_DELETE_BOOKS + if "passwd_role" in to_save: + content.role = content.role + ub.ROLE_PASSWD + if "edit_shelf_role" in to_save: + content.role = content.role + ub.ROLE_EDIT_SHELFS + if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: + flash(_(u"Please fill out all fields!"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + title=_(u"Add new user")) + content.password = generate_password_hash(to_save["password"]) + content.nickname = to_save["nickname"] + if config.config_public_reg and not check_valid_domain(to_save["email"]): + flash(_(u"E-mail is not from valid domain"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + title=_(u"Add new user")) + else: + content.email = to_save["email"] + try: + ub.session.add(content) + ub.session.commit() + flash(_(u"User '%(user)s' created", user=content.nickname), category="success") + return redirect(url_for('admin')) + except IntegrityError: + ub.session.rollback() + flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + else: + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + languages=languages, title=_(u"Add new user"), page="newuser") + + +@admi.route("/admin/mailsettings", methods=["GET", "POST"]) +@login_required +@admin_required +def edit_mailsettings(): + content = ub.session.query(ub.Settings).first() + if request.method == "POST": + to_save = request.form.to_dict() + content.mail_server = to_save["mail_server"] + content.mail_port = int(to_save["mail_port"]) + content.mail_login = to_save["mail_login"] + content.mail_password = to_save["mail_password"] + content.mail_from = to_save["mail_from"] + content.mail_use_ssl = int(to_save["mail_use_ssl"]) + try: + ub.session.commit() + except Exception as e: + flash(e, category="error") + if "test" in to_save and to_save["test"]: + if current_user.kindle_mail: + result = helper.send_test_mail(current_user.kindle_mail, current_user.nickname) + if result is None: + flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), + category="success") + else: + flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") + else: + flash(_(u"Please configure your kindle e-mail address first..."), category="error") + else: + flash(_(u"E-mail server settings updated"), category="success") + return render_title_template("email_edit.html", content=content, title=_(u"Edit e-mail server settings"), + page="mailset") + + +@admi.route("/admin/user/", methods=["GET", "POST"]) +@login_required +@admin_required +def edit_user(user_id): + content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User + downloads = list() + languages = speaking_language() + translations = babel.list_translations() + [LC('en')] + for book in content.downloads: + downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if downloadbook: + downloads.append(downloadbook) + else: + ub.delete_download(book.book_id) + # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete() + # ub.session.commit() + if request.method == "POST": + to_save = request.form.to_dict() + if "delete" in to_save: + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session.commit() + flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") + return redirect(url_for('admin')) + else: + if "password" in to_save and to_save["password"]: + content.password = generate_password_hash(to_save["password"]) + + if "admin_role" in to_save and not content.role_admin(): + content.role = content.role + ub.ROLE_ADMIN + elif "admin_role" not in to_save and content.role_admin(): + content.role = content.role - ub.ROLE_ADMIN + + if "download_role" in to_save and not content.role_download(): + content.role = content.role + ub.ROLE_DOWNLOAD + elif "download_role" not in to_save and content.role_download(): + content.role = content.role - ub.ROLE_DOWNLOAD + + if "upload_role" in to_save and not content.role_upload(): + content.role = content.role + ub.ROLE_UPLOAD + elif "upload_role" not in to_save and content.role_upload(): + content.role = content.role - ub.ROLE_UPLOAD + + if "edit_role" in to_save and not content.role_edit(): + content.role = content.role + ub.ROLE_EDIT + elif "edit_role" not in to_save and content.role_edit(): + content.role = content.role - ub.ROLE_EDIT + + if "delete_role" in to_save and not content.role_delete_books(): + content.role = content.role + ub.ROLE_DELETE_BOOKS + elif "delete_role" not in to_save and content.role_delete_books(): + content.role = content.role - ub.ROLE_DELETE_BOOKS + + if "passwd_role" in to_save and not content.role_passwd(): + content.role = content.role + ub.ROLE_PASSWD + elif "passwd_role" not in to_save and content.role_passwd(): + content.role = content.role - ub.ROLE_PASSWD + + if "edit_shelf_role" in to_save and not content.role_edit_shelfs(): + content.role = content.role + ub.ROLE_EDIT_SHELFS + elif "edit_shelf_role" not in to_save and content.role_edit_shelfs(): + content.role = content.role - ub.ROLE_EDIT_SHELFS + + if "show_random" in to_save and not content.show_random_books(): + content.sidebar_view += ub.SIDEBAR_RANDOM + elif "show_random" not in to_save and content.show_random_books(): + content.sidebar_view -= ub.SIDEBAR_RANDOM + + if "show_language" in to_save and not content.show_language(): + content.sidebar_view += ub.SIDEBAR_LANGUAGE + elif "show_language" not in to_save and content.show_language(): + content.sidebar_view -= ub.SIDEBAR_LANGUAGE + + if "show_series" in to_save and not content.show_series(): + content.sidebar_view += ub.SIDEBAR_SERIES + elif "show_series" not in to_save and content.show_series(): + content.sidebar_view -= ub.SIDEBAR_SERIES + + if "show_category" in to_save and not content.show_category(): + content.sidebar_view += ub.SIDEBAR_CATEGORY + elif "show_category" not in to_save and content.show_category(): + content.sidebar_view -= ub.SIDEBAR_CATEGORY + + if "show_recent" in to_save and not content.show_recent(): + content.sidebar_view += ub.SIDEBAR_RECENT + elif "show_recent" not in to_save and content.show_recent(): + content.sidebar_view -= ub.SIDEBAR_RECENT + + if "show_sorted" in to_save and not content.show_sorted(): + content.sidebar_view += ub.SIDEBAR_SORTED + elif "show_sorted" not in to_save and content.show_sorted(): + content.sidebar_view -= ub.SIDEBAR_SORTED + + if "show_publisher" in to_save and not content.show_publisher(): + content.sidebar_view += ub.SIDEBAR_PUBLISHER + elif "show_publisher" not in to_save and content.show_publisher(): + content.sidebar_view -= ub.SIDEBAR_PUBLISHER + + if "show_hot" in to_save and not content.show_hot_books(): + content.sidebar_view += ub.SIDEBAR_HOT + elif "show_hot" not in to_save and content.show_hot_books(): + content.sidebar_view -= ub.SIDEBAR_HOT + + if "show_best_rated" in to_save and not content.show_best_rated_books(): + content.sidebar_view += ub.SIDEBAR_BEST_RATED + elif "show_best_rated" not in to_save and content.show_best_rated_books(): + content.sidebar_view -= ub.SIDEBAR_BEST_RATED + + if "show_read_and_unread" in to_save and not content.show_read_and_unread(): + content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD + elif "show_read_and_unread" not in to_save and content.show_read_and_unread(): + content.sidebar_view -= ub.SIDEBAR_READ_AND_UNREAD + + if "show_author" in to_save and not content.show_author(): + content.sidebar_view += ub.SIDEBAR_AUTHOR + elif "show_author" not in to_save and content.show_author(): + content.sidebar_view -= ub.SIDEBAR_AUTHOR + + if "show_detail_random" in to_save and not content.show_detail_random(): + content.sidebar_view += ub.DETAIL_RANDOM + elif "show_detail_random" not in to_save and content.show_detail_random(): + content.sidebar_view -= ub.DETAIL_RANDOM + + content.mature_content = "show_mature_content" in to_save + + if "default_language" in to_save: + content.default_language = to_save["default_language"] + if "locale" in to_save and to_save["locale"]: + content.locale = to_save["locale"] + if to_save["email"] and to_save["email"] != content.email: + content.email = to_save["email"] + if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: + content.kindle_mail = to_save["kindle_mail"] + try: + ub.session.commit() + flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") + except IntegrityError: + ub.session.rollback() + flash(_(u"An unknown error occured."), category="error") + return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, + content=content, downloads=downloads, title=_(u"Edit User %(nick)s", + nick=content.nickname), page="edituser") + + +@admi.route("/admin/resetpassword/") +@login_required +@admin_required +def reset_password(user_id): + if not config.config_public_reg: + abort(404) + if current_user is not None and current_user.is_authenticated: + existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first() + password = helper.generate_random_password() + existing_user.password = generate_password_hash(password) + try: + ub.session.commit() + helper.send_registration_mail(existing_user.email, existing_user.nickname, password, True) + flash(_(u"Password for user %(user)s reset", user=existing_user.nickname), category="success") + except Exception: + ub.session.rollback() + flash(_(u"An unknown error occurred. Please try again later."), category="error") + return redirect(url_for('admin')) + + +@admi.route("/get_update_status", methods=['GET']) +@login_required_if_no_ano +def get_update_status(): + return updater_thread.get_available_updates(request.method) + + +@admi.route("/get_updater_status", methods=['GET', 'POST']) +@login_required +@admin_required +def get_updater_status(): + status = {} + if request.method == "POST": + commit = request.form.to_dict() + if "start" in commit and commit['start'] == 'True': + text = { + "1": _(u'Requesting update package'), + "2": _(u'Downloading update package'), + "3": _(u'Unzipping update package'), + "4": _(u'Replacing files'), + "5": _(u'Database connections are closed'), + "6": _(u'Stopping server'), + "7": _(u'Update finished, please press okay and reload page'), + "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), + "9": _(u'Update failed:') + u' ' + _(u'Connection error'), + "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), + "11": _(u'Update failed:') + u' ' + _(u'General error') + } + status['text'] = text + # helper.updater_thread = helper.Updater() + updater_thread.start() + status['status'] = updater_thread.get_update_status() + elif request.method == "GET": + try: + status['status'] = updater_thread.get_update_status() + except AttributeError: + # thread is not active, occurs after restart on update + status['status'] = 7 + except Exception: + status['status'] = 11 + return json.dumps(status) diff --git a/cps/book_formats.py b/cps/book_formats.py deleted file mode 100644 index 93febb4b..00000000 --- a/cps/book_formats.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) -# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski 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 . - -import logging -import uploader -import os -from flask_babel import gettext as _ -import comic - -try: - from lxml.etree import LXML_VERSION as lxmlversion -except ImportError: - lxmlversion = None - -__author__ = 'lemmsh' - -logger = logging.getLogger("book_formats") - -try: - from wand.image import Image - from wand import version as ImageVersion - use_generic_pdf_cover = False -except (ImportError, RuntimeError) as e: - logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) - use_generic_pdf_cover = True -try: - from PyPDF2 import PdfFileReader - from PyPDF2 import __version__ as PyPdfVersion - use_pdf_meta = True -except ImportError as e: - logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e) - use_pdf_meta = False - -try: - import epub - use_epub_meta = True -except ImportError as e: - logger.warning('cannot import epub, extracting epub metadata will not work: %s', e) - use_epub_meta = False - -try: - import fb2 - use_fb2_meta = True -except ImportError as e: - logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) - use_fb2_meta = False - - -def process(tmp_file_path, original_file_name, original_file_extension): - meta = None - try: - if ".PDF" == original_file_extension.upper(): - meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension) - if ".EPUB" == original_file_extension.upper() and use_epub_meta is True: - meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension) - if ".FB2" == original_file_extension.upper() and use_fb2_meta is True: - meta = fb2.get_fb2_info(tmp_file_path, original_file_extension) - if original_file_extension.upper() in ['.CBZ', '.CBT']: - meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension) - - except Exception as ex: - logger.warning('cannot parse metadata, using default: %s', ex) - - if meta and meta.title.strip() and meta.author.strip(): - return meta - else: - return default_meta(tmp_file_path, original_file_name, original_file_extension) - - -def default_meta(tmp_file_path, original_file_name, original_file_extension): - return uploader.BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=original_file_name, - author=u"Unknown", - cover=None, - description="", - tags="", - series="", - series_id="", - languages="") - - -def pdf_meta(tmp_file_path, original_file_name, original_file_extension): - - if use_pdf_meta: - pdf = PdfFileReader(open(tmp_file_path, 'rb')) - doc_info = pdf.getDocumentInfo() - else: - doc_info = None - - if doc_info is not None: - author = doc_info.author if doc_info.author else u"Unknown" - title = doc_info.title if doc_info.title else original_file_name - subject = doc_info.subject - else: - author = u"Unknown" - title = original_file_name - subject = "" - return uploader.BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=title, - author=author, - cover=pdf_preview(tmp_file_path, original_file_name), - description=subject, - tags="", - series="", - series_id="", - languages="") - - -def pdf_preview(tmp_file_path, tmp_dir): - if use_generic_pdf_cover: - return None - else: - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" - with Image(filename=tmp_file_path + "[0]", resolution=150) as img: - img.compression_quality = 88 - img.save(filename=os.path.join(tmp_dir, cover_file_name)) - return cover_file_name - - -def get_versions(): - if not use_generic_pdf_cover: - IVersion = ImageVersion.MAGICK_VERSION - WVersion = ImageVersion.VERSION - else: - IVersion = _(u'not installed') - WVersion = _(u'not installed') - if use_pdf_meta: - PVersion='v'+PyPdfVersion - else: - PVersion=_(u'not installed') - if lxmlversion: - XVersion = 'v'+'.'.join(map(str, lxmlversion)) - else: - XVersion = _(u'not installed') - return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion} diff --git a/cps/converter.py b/cps/converter.py index 29c371bb..e1012448 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -24,13 +24,14 @@ import ub import re from flask_babel import gettext as _ from subproc_wrapper import process_open +from cps import config def versionKindle(): versions = _(u'not installed') - if os.path.exists(ub.config.config_converterpath): + if os.path.exists(config.config_converterpath): try: - p = process_open(ub.config.config_converterpath) + p = process_open(config.config_converterpath) # p = subprocess.Popen(ub.config.config_converterpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() for lines in p.stdout.readlines(): @@ -45,9 +46,9 @@ def versionKindle(): def versionCalibre(): versions = _(u'not installed') - if os.path.exists(ub.config.config_converterpath): + if os.path.exists(config.config_converterpath): try: - p = process_open([ub.config.config_converterpath, '--version']) + p = process_open([config.config_converterpath, '--version']) # p = subprocess.Popen([ub.config.config_converterpath, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() for lines in p.stdout.readlines(): @@ -61,9 +62,9 @@ def versionCalibre(): def versioncheck(): - if ub.config.config_ebookconverter == 1: + if config.config_ebookconverter == 1: return versionKindle() - elif ub.config.config_ebookconverter == 2: + elif config.config_ebookconverter == 2: return versionCalibre() else: return {'ebook_converter':_(u'not configured')} diff --git a/cps/editbooks.py b/cps/editbooks.py new file mode 100644 index 00000000..4ca5af1f --- /dev/null +++ b/cps/editbooks.py @@ -0,0 +1,756 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# opds routing functions +from cps import config, language_table, get_locale, app, ub +from flask import request, flash, redirect, url_for, abort, Markup +from flask import Blueprint +import datetime +import db +import os +from flask_babel import gettext as _ +from uuid import uuid4 +import helper +from flask_login import current_user +from web import login_required_if_no_ano, common_filters, order_authors, render_title_template, edit_required, \ + upload_required, login_required +import gdriveutils +from shutil import move, copyfile +import uploader +from iso639 import languages as isoLanguages + +editbook = Blueprint('editbook', __name__) + +EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'} + +EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', + 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} + + + +# Modifies different Database objects, first check if elements have to be added to database, than check +# if elements have to be deleted, because they are no longer used +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 + if not isinstance(input_elements, list): + raise TypeError(str(input_elements) + " should be passed as a list") + + input_elements = [x for x in input_elements if x != ''] + # we have all input element (authors, series, tags) names now + # 1. search for elements to remove + del_elements = [] + for c_elements in db_book_object: + found = False + if db_type == 'languages': + type_elements = c_elements.lang_code + elif db_type == 'custom': + type_elements = c_elements.value + else: + type_elements = c_elements.name + for inp_element in input_elements: + if inp_element.lower() == type_elements.lower(): + # if inp_element == type_elements: + found = True + break + # if the element was not found in the new list, add it to remove list + if not found: + del_elements.append(c_elements) + # 2. search for elements that need to be added + add_elements = [] + for inp_element in input_elements: + found = False + for c_elements in db_book_object: + if db_type == 'languages': + type_elements = c_elements.lang_code + elif db_type == 'custom': + type_elements = c_elements.value + else: + type_elements = c_elements.name + if inp_element == type_elements: + found = True + break + if not found: + add_elements.append(inp_element) + # if there are elements to remove, we remove them now + if len(del_elements) > 0: + for del_element in del_elements: + db_book_object.remove(del_element) + if len(del_element.books) == 0: + db_session.delete(del_element) + # if there are elements to add, we add them now! + if len(add_elements) > 0: + if db_type == 'languages': + db_filter = db_object.lang_code + elif db_type == 'custom': + db_filter = db_object.value + else: + db_filter = db_object.name + for add_element in add_elements: + # check if a element with that name exists + db_element = db_session.query(db_object).filter(db_filter == add_element).first() + # if no element is found add it + # if new_element is None: + if db_type == 'author': + new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") + elif db_type == 'series': + new_element = db_object(add_element, add_element) + elif db_type == 'custom': + new_element = db_object(value=add_element) + elif db_type == 'publisher': + new_element = db_object(add_element, None) + else: # db_type should be tag or language + new_element = db_object(add_element) + if db_element is None: + db_session.add(new_element) + db_book_object.append(new_element) + else: + if db_type == 'custom': + if db_element.value != add_element: + new_element.value = add_element + # new_element = db_element + elif db_type == 'languages': + if db_element.lang_code != add_element: + db_element.lang_code = add_element + # new_element = db_element + elif db_type == 'series': + if db_element.name != add_element: + db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element) + db_element.sort = add_element + # new_element = db_element + elif db_type == 'author': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element.replace('|', ',') + # new_element = db_element + elif db_type == 'publisher': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = None + # new_element = db_element + elif db_element.name != add_element: + db_element.name = add_element + # new_element = db_element + # add element to book + db_book_object.append(db_element) + + +@editbook.route("/delete//", defaults={'book_format': ""}) +@editbook.route("/delete///") +@login_required +def delete_book(book_id, book_format): + if current_user.role_delete_books(): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + if book: + helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) + if not book_format: + # delete book from Shelfs, Downloads, Read list + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() + ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete() + ub.delete_download(book_id) + ub.session.commit() + + # check if only this book links to: + # author, language, series, tags, custom columns + modify_database_object([u''], book.authors, db.Authors, db.session, 'author') + modify_database_object([u''], book.tags, db.Tags, db.session, 'tags') + modify_database_object([u''], book.series, db.Series, db.session, 'series') + modify_database_object([u''], book.languages, db.Languages, db.session, 'languages') + modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers') + + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + for c in cc: + cc_string = "custom_column_" + str(c.id) + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + if c.datatype == 'bool' or c.datatype == 'integer': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + elif c.datatype == 'rating': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + else: + modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], + db.session, 'custom') + db.session.query(db.Books).filter(db.Books.id == book_id).delete() + else: + db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete() + db.session.commit() + else: + # book not found + app.logger.info('Book with id "'+str(book_id)+'" could not be deleted') + if book_format: + return redirect(url_for('edit_book', book_id=book_id)) + else: + return redirect(url_for('index')) + + +def render_edit_book(book_id): + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + book = db.session.query(db.Books)\ + .filter(db.Books.id == book_id).filter(common_filters()).first() + + if not book: + flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + return redirect(url_for("web.index")) + + for indx in range(0, len(book.languages)): + book.languages[indx].language_name = language_table[get_locale()][book.languages[indx].lang_code] + + book = order_authors(book) + + author_names = [] + for authr in book.authors: + author_names.append(authr.name.replace('|', ',')) + + # Option for showing convertbook button + valid_source_formats=list() + if config.config_ebookconverter == 2: + for file in book.data: + if file.format.lower() in EXTENSIONS_CONVERT: + valid_source_formats.append(file.format.lower()) + + # Determine what formats don't already exist + allowed_conversion_formats = EXTENSIONS_CONVERT.copy() + for file in book.data: + try: + allowed_conversion_formats.remove(file.format.lower()) + except Exception: + app.logger.warning(file.format.lower() + ' already removed from list.') + + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, + title=_(u"edit metadata"), page="editbook", + conversion_formats=allowed_conversion_formats, + source_formats=valid_source_formats) + + +def edit_cc_data(book_id, book, to_save): + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + for c in cc: + cc_string = "custom_column_" + str(c.id) + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + cc_db_value = getattr(book, cc_string)[0].value + else: + cc_db_value = None + if to_save[cc_string].strip(): + if c.datatype == 'bool': + if to_save[cc_string] == 'None': + to_save[cc_string] = None + else: + to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0 + if to_save[cc_string] != cc_db_value: + if cc_db_value is not None: + if to_save[cc_string] is not None: + setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + else: + cc_class = db.cc_classes[c.id] + new_cc = cc_class(value=to_save[cc_string], book=book_id) + db.session.add(new_cc) + elif c.datatype == 'int': + if to_save[cc_string] == 'None': + to_save[cc_string] = None + if to_save[cc_string] != cc_db_value: + if cc_db_value is not None: + if to_save[cc_string] is not None: + setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + else: + cc_class = db.cc_classes[c.id] + new_cc = cc_class(value=to_save[cc_string], book=book_id) + db.session.add(new_cc) + + else: + if c.datatype == 'rating': + to_save[cc_string] = str(int(float(to_save[cc_string]) * 2)) + if to_save[cc_string].strip() != cc_db_value: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + cc_class = db.cc_classes[c.id] + new_cc = db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # if no cc val is found add it + if new_cc is None: + new_cc = cc_class(value=to_save[cc_string].strip()) + db.session.add(new_cc) + db.session.flush() + new_cc = db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # add cc value to book + getattr(book, cc_string).append(new_cc) + else: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + else: + input_tags = to_save[cc_string].split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session, + 'custom') + return cc + +def upload_single_file(request, book, book_id): + # Check and handle Uploaded file + if 'btn-upload-format' in request.files: + requested_file = request.files['btn-upload-format'] + # check for empty request + if requested_file.filename != '': + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in EXTENSIONS_UPLOAD: + flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), + category="error") + return redirect(url_for('show_book', book_id=book.id)) + else: + flash(_('File to be uploaded must have an extension'), category="error") + return redirect(url_for('show_book', book_id=book.id)) + + file_name = book.path.rsplit('/', 1)[-1] + filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) + saved_filename = os.path.join(filepath, file_name + '.' + file_ext) + + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): + try: + os.makedirs(filepath) + except OSError: + flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") + return redirect(url_for('show_book', book_id=book.id)) + try: + requested_file.save(saved_filename) + except OSError: + flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") + return redirect(url_for('show_book', book_id=book.id)) + + file_size = os.path.getsize(saved_filename) + is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\ + filter(db.Data.format == file_ext.upper()).first() + + # Format entry already exists, no need to update the database + if is_format: + app.logger.info('Book format already existing') + else: + db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) + db.session.add(db_format) + db.session.commit() + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + + # Queue uploader info + uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) + helper.global_WorkerThread.add_upload(current_user.nickname, + "" + uploadText + "") + +def upload_cover(request, book): + if 'btn-upload-cover' in request.files: + requested_file = request.files['btn-upload-cover'] + # check for empty request + if requested_file.filename != '': + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) + saved_filename = os.path.join(filepath, 'cover.' + file_ext) + + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): + try: + os.makedirs(filepath) + except OSError: + flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), + category="error") + return redirect(url_for('show_book', book_id=book.id)) + try: + requested_file.save(saved_filename) + # im=Image.open(saved_filename) + book.has_cover = 1 + except OSError: + flash(_(u"Failed to store cover-file %(cover)s.", cover=saved_filename), category="error") + return redirect(url_for('show_book', book_id=book.id)) + except IOError: + flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error") + return redirect(url_for('show_book', book_id=book.id)) + +@editbook.route("/admin/book/", methods=['GET', 'POST']) +@login_required_if_no_ano +@edit_required +def edit_book(book_id): + # Show form + if request.method != 'POST': + return render_edit_book(book_id) + + # create the function for sorting... + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + book = db.session.query(db.Books)\ + .filter(db.Books.id == book_id).filter(common_filters()).first() + + # Book not found + if not book: + flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + return redirect(url_for("web.index")) + + upload_single_file(request, book, book_id) + upload_cover(request, book) + try: + to_save = request.form.to_dict() + # Update book + edited_books_id = None + #handle book title + if book.title != to_save["book_title"].rstrip().strip(): + if to_save["book_title"] == '': + to_save["book_title"] = _(u'unknown') + book.title = to_save["book_title"].rstrip().strip() + edited_books_id = book.id + + # handle author(s) + input_authors = to_save["author_name"].split('&') + input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) + # we have all author names now + if input_authors == ['']: + input_authors = [_(u'unknown')] # prevent empty Author + + modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author') + + # Search for each author if author is in database, if not, authorname and sorted authorname is generated new + # everything then is assembled for sorted author field in database + sort_authors_list = list() + for inp in input_authors: + stored_author = db.session.query(db.Authors).filter(db.Authors.name == inp).first() + if not stored_author: + stored_author = helper.get_sorted_author(inp) + else: + stored_author = stored_author.sort + sort_authors_list.append(helper.get_sorted_author(stored_author)) + sort_authors = ' & '.join(sort_authors_list) + if book.author_sort != sort_authors: + edited_books_id = book.id + book.author_sort = sort_authors + + + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + + error = False + if edited_books_id: + error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0]) + + if not error: + if to_save["cover_url"]: + if helper.save_cover(to_save["cover_url"], book.path) is True: + book.has_cover = 1 + else: + flash(_(u"Cover is not a jpg file, can't save"), category="error") + + if book.series_index != to_save["series_index"]: + book.series_index = to_save["series_index"] + + # Handle book comments/description + if len(book.comments): + book.comments[0].text = to_save["description"] + else: + book.comments.append(db.Comments(text=to_save["description"], book=book.id)) + + # Handle book tags + input_tags = to_save["tags"].split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags') + + # Handle book series + input_series = [to_save["series"].strip()] + input_series = [x for x in input_series if x != ''] + modify_database_object(input_series, book.series, db.Series, db.session, 'series') + + if to_save["pubdate"]: + try: + book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d") + except ValueError: + book.pubdate = db.Books.DEFAULT_PUBDATE + else: + book.pubdate = db.Books.DEFAULT_PUBDATE + + if to_save["publisher"]: + publisher = to_save["publisher"].rstrip().strip() + if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): + modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher') + elif len(book.publishers): + modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher') + + + # handle book languages + input_languages = to_save["languages"].split(',') + input_languages = [x.strip().lower() for x in input_languages if x != ''] + input_l = [] + invers_lang_table = [x.lower() for x in language_table[get_locale()].values()] + for lang in input_languages: + try: + res = list(language_table[get_locale()].keys())[invers_lang_table.index(lang)] + input_l.append(res) + except ValueError: + app.logger.error('%s is not a valid language' % lang) + flash(_(u"%(langname)s is not a valid language", langname=lang), category="error") + modify_database_object(input_l, book.languages, db.Languages, db.session, 'languages') + + # handle book ratings + if to_save["rating"].strip(): + old_rating = False + if len(book.ratings) > 0: + old_rating = book.ratings[0].rating + ratingx2 = int(float(to_save["rating"]) * 2) + if ratingx2 != old_rating: + is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first() + if is_rating: + book.ratings.append(is_rating) + else: + new_rating = db.Ratings(rating=ratingx2) + book.ratings.append(new_rating) + if old_rating: + book.ratings.remove(book.ratings[0]) + else: + if len(book.ratings) > 0: + book.ratings.remove(book.ratings[0]) + + # handle cc data + edit_cc_data(book_id, book, to_save) + + db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if "detail_view" in to_save: + return redirect(url_for('show_book', book_id=book.id)) + else: + flash(_("Metadata successfully updated"), category="success") + return render_edit_book(book_id) + else: + db.session.rollback() + flash(error, category="error") + return render_edit_book(book_id) + except Exception as e: + app.logger.exception(e) + db.session.rollback() + flash(_("Error editing book, please check logfile for details"), category="error") + return redirect(url_for('show_book', book_id=book.id)) + + +@editbook.route("/upload", methods=["GET", "POST"]) +@login_required_if_no_ano +@upload_required +def upload(): + if not config.config_uploading: + abort(404) + if request.method == 'POST' and 'btn-upload' in request.files: + for requested_file in request.files.getlist("btn-upload"): + # create the function for sorting... + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) + + # check if file extension is correct + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in EXTENSIONS_UPLOAD: + flash( + _("File extension '%(ext)s' is not allowed to be uploaded to this server", + ext=file_ext), category="error") + return redirect(url_for('index')) + else: + flash(_('File to be uploaded must have an extension'), category="error") + return redirect(url_for('index')) + + # extract metadata from file + meta = uploader.upload(requested_file) + title = meta.title + authr = meta.author + tags = meta.tags + series = meta.series + series_index = meta.series_id + title_dir = helper.get_valid_filename(title) + author_dir = helper.get_valid_filename(authr) + filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir) + saved_filename = os.path.join(filepath, title_dir + meta.extension.lower()) + + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): + try: + os.makedirs(filepath) + except OSError: + flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") + return redirect(url_for('index')) + try: + copyfile(meta.file_path, saved_filename) + except OSError: + flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") + return redirect(url_for('index')) + try: + os.unlink(meta.file_path) + except OSError: + flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path), + category="warning") + + if meta.cover is None: + has_cover = 0 + copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"), + os.path.join(filepath, "cover.jpg")) + else: + has_cover = 1 + move(meta.cover, os.path.join(filepath, "cover.jpg")) + + # handle authors + is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first() + if is_author: + db_author = is_author + else: + db_author = db.Authors(authr, helper.get_sorted_author(authr), "") + db.session.add(db_author) + + # handle series + db_series = None + is_series = db.session.query(db.Series).filter(db.Series.name == series).first() + if is_series: + db_series = is_series + elif series != '': + db_series = db.Series(series, "") + db.session.add(db_series) + + # add language actually one value in list + input_language = meta.languages + db_language = None + if input_language != "": + input_language = isoLanguages.get(name=input_language).part3 + hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first() + if hasLanguage: + db_language = hasLanguage + else: + db_language = db.Languages(input_language) + db.session.add(db_language) + + # combine path and normalize path from windows systems + path = os.path.join(author_dir, title_dir).replace('\\', '/') + db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), + series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language) + db_book.authors.append(db_author) + if db_series: + db_book.series.append(db_series) + if db_language is not None: + db_book.languages.append(db_language) + file_size = os.path.getsize(saved_filename) + db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) + + # handle tags + input_tags = tags.split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + if input_tags[0] !="": + modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags') + + # flush content, get db_book.id available + db_book.data.append(db_data) + db.session.add(db_book) + db.session.flush() + + # add comment + book_id = db_book.id + upload_comment = Markup(meta.description).unescape() + if upload_comment != "": + db.session.add(db.Comments(upload_comment, book_id)) + + # save data to database, reread data + db.session.commit() + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + + # upload book to gdrive if nesseccary and add "(bookid)" to folder name + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + error = helper.update_dir_stucture(book.id, config.config_calibre_dir) + db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if error: + flash(error, category="error") + uploadText=_(u"File %(file)s uploaded", file=book.title) + helper.global_WorkerThread.add_upload(current_user.nickname, + "" + uploadText + "") + + # create data for displaying display Full language name instead of iso639.part3language + if db_language is not None: + book.languages[0].language_name = _(meta.languages) + author_names = [] + for author in db_book.authors: + author_names.append(author.name) + if len(request.files.getlist("btn-upload")) < 2: + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns. + datatype.notin_(db.cc_exceptions)).all() + if current_user.role_edit() or current_user.role_admin(): + return render_title_template('book_edit.html', book=book, authors=author_names, + cc=cc, title=_(u"edit metadata"), page="upload") + book_in_shelfs = [] + kindle_list = helper.check_send_to_kindle(book) + reader_list = helper.check_read_formats(book) + + return render_title_template('detail.html', entry=book, cc=cc, + title=book.title, books_shelfs=book_in_shelfs, kindle_list=kindle_list, + reader_list=reader_list, page="upload") + return redirect(url_for("web.index")) + + +@editbook.route("/admin/book/convert/", methods=['POST']) +@login_required_if_no_ano +@edit_required +def convert_bookformat(book_id): + # check to see if we have form fields to work with - if not send user back + book_format_from = request.form.get('book_format_from', None) + book_format_to = request.form.get('book_format_to', None) + + if (book_format_from is None) or (book_format_to is None): + flash(_(u"Source or destination format for conversion missing"), category="error") + return redirect(request.environ["HTTP_REFERER"]) + + app.logger.debug('converting: book id: ' + str(book_id) + + ' from: ' + request.form['book_format_from'] + + ' to: ' + request.form['book_format_to']) + rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), + book_format_to.upper(), current_user.nickname) + + if rtn is None: + flash(_(u"Book successfully queued for converting to %(book_format)s", + book_format=book_format_to), + category="success") + else: + flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") + return redirect(request.environ["HTTP_REFERER"]) diff --git a/cps/gdrive.py b/cps/gdrive.py new file mode 100644 index 00000000..3cd9c1dc --- /dev/null +++ b/cps/gdrive.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import os +from flask import Blueprint +import gdriveutils +from flask import flash, request, redirect, url_for, abort +from flask_babel import gettext as _ +from cps import app, config, ub, db +from flask_login import login_required +import json +from uuid import uuid4 +from time import time +import tempfile +from shutil import move, copyfile +from web import admin_required + +try: + from googleapiclient.errors import HttpError +except ImportError: + pass + +gdrive = Blueprint('gdrive', __name__) + +current_milli_time = lambda: int(round(time() * 1000)) + +gdrive_watch_callback_token = 'target=calibreweb-watch_files' + + +@gdrive.route("/gdrive/authenticate") +@login_required +@admin_required +def authenticate_google_drive(): + try: + authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl() + except gdriveutils.InvalidConfigError: + flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'), + category="error") + return redirect(url_for('web.index')) + return redirect(authUrl) + + +@gdrive.route("/gdrive/callback") +def google_drive_callback(): + auth_code = request.args.get('code') + if not auth_code: + abort(403) + try: + credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code) + with open(os.path.join(config.get_main_dir,'gdrive_credentials'), 'w') as f: + f.write(credentials.to_json()) + except ValueError as error: + app.logger.error(error) + return redirect(url_for('configuration')) + + +@gdrive.route("/gdrive/watch/subscribe") +@login_required +@admin_required +def watch_gdrive(): + if not config.config_google_drive_watch_changes_response: + with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings: + filedata = json.load(settings) + if filedata['web']['redirect_uris'][0].endswith('/'): + filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] + else: + filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))] + address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0] + notification_id = str(uuid4()) + try: + result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, + 'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000) + settings = ub.session.query(ub.Settings).first() + settings.config_google_drive_watch_changes_response = json.dumps(result) + ub.session.merge(settings) + ub.session.commit() + settings = ub.session.query(ub.Settings).first() + config.loadSettings() + except HttpError as e: + reason=json.loads(e.content)['error']['errors'][0] + if reason['reason'] == u'push.webhookUrlUnauthorized': + flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error") + else: + flash(reason['message'], category="error") + + return redirect(url_for('configuration')) + + +@gdrive.route("/gdrive/watch/revoke") +@login_required +@admin_required +def revoke_watch_gdrive(): + last_watch_response = config.config_google_drive_watch_changes_response + if last_watch_response: + try: + gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], + last_watch_response['resourceId']) + except HttpError: + pass + settings = ub.session.query(ub.Settings).first() + settings.config_google_drive_watch_changes_response = None + ub.session.merge(settings) + ub.session.commit() + config.loadSettings() + return redirect(url_for('configuration')) + + +@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) +def on_received_watch_confirmation(): + app.logger.debug(request.headers) + if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \ + and request.headers.get('X-Goog-Resource-State') == 'change' \ + and request.data: + + data = request.data + + def updateMetaData(): + app.logger.info('Change received from gdrive') + app.logger.debug(data) + try: + j = json.loads(data) + app.logger.info('Getting change details') + response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) + app.logger.debug(response) + if response: + dbpath = os.path.join(config.config_calibre_dir, "metadata.db") + if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath): + tmpDir = tempfile.gettempdir() + app.logger.info('Database file updated') + copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) + app.logger.info('Backing up existing and downloading updated metadata.db') + gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) + app.logger.info('Setting up new DB') + # prevent error on windows, as os.rename does on exisiting files + move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) + db.setup_db() + except Exception as e: + app.logger.info(e.message) + app.logger.exception(e) + updateMetaData() + return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 1f0b8b83..d622863a 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -27,7 +27,7 @@ except ImportError: gdrive_support = False import os -from cps import config +from cps import config, app import cli import shutil from flask import Response, stream_with_context @@ -37,8 +37,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * -import web - class Singleton: """ A non-thread-safe helper class to ease implementing singletons. @@ -89,6 +87,10 @@ class Gdrive: def __init__(self): self.drive = getDrive(gauth=Gauth.Instance().auth) +def is_gdrive_ready(): + return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \ + os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials')) + engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False) Base = declarative_base() @@ -157,9 +159,9 @@ def getDrive(drive=None, gauth=None): try: gauth.Refresh() except RefreshError as e: - web.app.logger.error("Google Drive error: " + e.message) + app.logger.error("Google Drive error: " + e.message) except Exception as e: - web.app.logger.exception(e) + app.logger.exception(e) else: # Initialize the saved creds gauth.Authorize() @@ -169,7 +171,7 @@ def getDrive(drive=None, gauth=None): try: drive.auth.Refresh() except RefreshError as e: - web.app.logger.error("Google Drive error: " + e.message) + app.logger.error("Google Drive error: " + e.message) return drive def listRootFolders(): @@ -206,7 +208,7 @@ def getEbooksFolderId(drive=None): try: gDriveId.gdrive_id = getEbooksFolder(drive)['id'] except Exception: - web.app.logger.error('Error gDrive, root ID not found') + app.logger.error('Error gDrive, root ID not found') gDriveId.path = '/' session.merge(gDriveId) session.commit() @@ -455,10 +457,10 @@ def getChangeById (drive, change_id): change = drive.auth.service.changes().get(changeId=change_id).execute() return change except (errors.HttpError) as error: - web.app.logger.info(error.message) + app.logger.info(error.message) return None except Exception as e: - web.app.logger.info(e) + app.logger.info(e) return None @@ -527,6 +529,6 @@ def do_gdrive_download(df, headers): if resp.status == 206: yield content else: - web.app.logger.info('An error occurred: %s' % resp) + app.logger.info('An error occurred: %s' % resp) return return Response(stream_with_context(stream()), headers=headers) diff --git a/cps/jinjia.py b/cps/jinjia.py new file mode 100644 index 00000000..89c15672 --- /dev/null +++ b/cps/jinjia.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# custom jinja filters + +from flask import Blueprint, request, url_for +import datetime +import re +from cps import mimetypes +from babel.dates import format_date +from flask_babel import get_locale + +jinjia = Blueprint('jinjia', __name__) + + +# pagination links in jinja +@jinjia.app_template_filter('url_for_other_page') +def url_for_other_page(page): + args = request.view_args.copy() + args['page'] = page + return url_for(request.endpoint, **args) + + +# shortentitles to at longest nchar, shorten longer words if necessary +@jinjia.app_template_filter('shortentitle') +def shortentitle_filter(s, nchar=20): + text = s.split() + res = "" # result + suml = 0 # overall length + for line in text: + if suml >= 60: + res += '...' + break + # if word longer than 20 chars truncate line and append '...', otherwise add whole word to result + # string, and summarize total length to stop at chars given by nchar + if len(line) > nchar: + res += line[:(nchar-3)] + '[..] ' + suml += nchar+3 + else: + res += line + ' ' + suml += len(line) + 1 + return res.strip() + + +@jinjia.app_template_filter('mimetype') +def mimetype_filter(val): + try: + s = mimetypes.types_map['.' + val] + except Exception: + s = 'application/octet-stream' + return s + + +@jinjia.app_template_filter('formatdate') +def formatdate_filter(val): + conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) + formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") + return format_date(formatdate, format='medium', locale=get_locale()) + + +@jinjia.app_template_filter('formatdateinput') +def format_date_input(val): + conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) + date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") + input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900 + return '' if input_date == "0101-01-01" else input_date + + +@jinjia.app_template_filter('strftime') +def timestamptodate(date, fmt=None): + date = datetime.datetime.fromtimestamp( + int(date)/1000 + ) + native = date.replace(tzinfo=None) + if fmt: + time_format = fmt + else: + time_format = '%d %m %Y - %H:%S' + return native.strftime(time_format) + + +@jinjia.app_template_filter('yesno') +def yesno(value, yes, no): + return yes if value else no + + +'''@jinjia.app_template_filter('canread') +def canread(ext): + if isinstance(ext, db.Data): + ext = ext.format + return ext.lower() in EXTENSIONS_READER''' diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py new file mode 100644 index 00000000..34e71fde --- /dev/null +++ b/cps/oauth_bb.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +from flask_dance.contrib.github import make_github_blueprint, github +from flask_dance.contrib.google import make_google_blueprint, google +from flask_dance.consumer import oauth_authorized, oauth_error +from sqlalchemy.orm.exc import NoResultFound +from oauth import OAuthBackend +from flask import flash, session, redirect, url_for, request +from cps import config, app +import ub +from flask_login import login_user, login_required, current_user +from flask_babel import gettext as _ +from web import github_oauth_required + + +oauth_check = {} + +def register_oauth_blueprint(blueprint, show_name): + if blueprint.name != "": + oauth_check[blueprint.name] = show_name + + +def register_user_with_oauth(user=None): + all_oauth = {} + for oauth in oauth_check.keys(): + if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '': + all_oauth[oauth] = oauth_check[oauth] + if len(all_oauth.keys()) == 0: + return + if user is None: + flash(_(u"Register with %s" % ", ".join(list(all_oauth.values()))), category="success") + else: + for oauth in all_oauth.keys(): + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=oauth, + provider_user_id=session[oauth + "_oauth_user_id"], + ) + try: + oauth = query.one() + oauth.user_id = user.id + except NoResultFound: + # no found, return error + return + try: + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + + +def logout_oauth_user(): + for oauth in oauth_check.keys(): + if oauth + '_oauth_user_id' in session: + session.pop(oauth + '_oauth_user_id') + + +github_blueprint = make_github_blueprint( + client_id=config.config_github_oauth_client_id, + client_secret=config.config_github_oauth_client_secret, + redirect_to="github_login",) + +google_blueprint = make_google_blueprint( + client_id=config.config_google_oauth_client_id, + client_secret=config.config_google_oauth_client_secret, + redirect_to="google_login", + scope=[ + "https://www.googleapis.com/auth/plus.me", + "https://www.googleapis.com/auth/userinfo.email", + ] +) + +app.register_blueprint(google_blueprint, url_prefix="/login") +app.register_blueprint(github_blueprint, url_prefix='/login') + +github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) +google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + + +if config.config_use_github_oauth: + register_oauth_blueprint(github_blueprint, 'GitHub') +if config.config_use_google_oauth: + register_oauth_blueprint(google_blueprint, 'Google') + + +@oauth_authorized.connect_via(github_blueprint) +def github_logged_in(blueprint, token): + if not token: + flash(_("Failed to log in with GitHub."), category="error") + return False + + resp = blueprint.session.get("/user") + if not resp.ok: + flash(_("Failed to fetch user info from GitHub."), category="error") + return False + + github_info = resp.json() + github_user_id = str(github_info["id"]) + return oauth_update_token(blueprint, token, github_user_id) + + +@oauth_authorized.connect_via(google_blueprint) +def google_logged_in(blueprint, token): + if not token: + flash(_("Failed to log in with Google."), category="error") + return False + + resp = blueprint.session.get("/oauth2/v2/userinfo") + if not resp.ok: + flash(_("Failed to fetch user info from Google."), category="error") + return False + + google_info = resp.json() + google_user_id = str(google_info["id"]) + + return oauth_update_token(blueprint, token, google_user_id) + + +def oauth_update_token(blueprint, token, provider_user_id): + session[blueprint.name + "_oauth_user_id"] = provider_user_id + session[blueprint.name + "_oauth_token"] = token + + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=blueprint.name, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # update token + oauth.token = token + except NoResultFound: + oauth = ub.OAuth( + provider=blueprint.name, + provider_user_id=provider_user_id, + token=token, + ) + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + + # Disable Flask-Dance's default behavior for saving the OAuth token + return False + + +def bind_oauth_or_register(provider, provider_user_id, redirect_url): + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # already bind with user, just login + if oauth.user: + login_user(oauth.user) + return redirect(url_for('index')) + else: + # bind to current user + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + return redirect(url_for('register')) + except NoResultFound: + return redirect(url_for(redirect_url)) + + +def get_oauth_status(): + status = [] + query = ub.session.query(ub.OAuth).filter_by( + user_id=current_user.id, + ) + try: + oauths = query.all() + for oauth in oauths: + status.append(oauth.provider) + return status + except NoResultFound: + return None + + +def unlink_oauth(provider): + if request.host_url + 'me' != request.referrer: + pass + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + user_id=current_user.id, + ) + try: + oauth = query.one() + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.delete(oauth) + ub.session.commit() + logout_oauth_user() + flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") + except NoResultFound: + app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) + flash(_("Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") + return redirect(url_for('profile')) + + +# notify on OAuth provider error +@oauth_error.connect_via(github_blueprint) +def github_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") + + +@web.route('/github') +@github_oauth_required +def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + return redirect(url_for('login')) + + +@web.route('/unlink/github', methods=["GET"]) +@login_required +def github_login_unlink(): + return unlink_oauth(github_blueprint.name) + + +@web.route('/google') +@google_oauth_required +def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') + flash(_(u"Google Oauth error, please retry later."), category="error") + return redirect(url_for('login')) + + +@oauth_error.connect_via(google_blueprint) +def google_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") + + +@web.route('/unlink/google', methods=["GET"]) +@login_required +def google_login_unlink(): + return unlink_oauth(google_blueprint.name) diff --git a/cps/opds.py b/cps/opds.py new file mode 100644 index 00000000..dd6ed984 --- /dev/null +++ b/cps/opds.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# opds routing functions +from cps import config, mimetypes, app +from flask import request, render_template, Response, g, make_response +from pagination import Pagination +from flask import Blueprint +import datetime +import db +import ub +from flask_login import current_user +from functools import wraps +from web import login_required_if_no_ano, fill_indexpage, common_filters, get_search_results, render_read_books +from sqlalchemy.sql.expression import func +import helper +from werkzeug.security import check_password_hash +from werkzeug.datastructures import Headers +try: + from urllib.parse import quote + from imp import reload +except ImportError: + from urllib import quote + +opds = Blueprint('opds', __name__) + + +def requires_basic_auth_if_no_ano(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if config.config_anonbrowse != 1: + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + return f(*args, **kwargs) + + return decorated + + +@opds.route("/opds") +@requires_basic_auth_if_no_ano +def feed_index(): + return render_xml_template('index.xml') + + +@opds.route("/opds/osd") +@requires_basic_auth_if_no_ano +def feed_osd(): + return render_xml_template('osd.xml', lang='en-EN') + + +@opds.route("/opds/search/") +@requires_basic_auth_if_no_ano +def feed_cc_search(query): + return feed_search(query.strip()) + + +@opds.route("/opds/search", methods=["GET"]) +@requires_basic_auth_if_no_ano +def feed_normal_search(): + return feed_search(request.args.get("query").strip()) + + +@opds.route("/opds/new") +@requires_basic_auth_if_no_ano +def feed_new(): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, True, [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/discover") +@requires_basic_auth_if_no_ano +def feed_discover(): + entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\ + .limit(config.config_books_per_page) + pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/rated") +@requires_basic_auth_if_no_ano +def feed_best_rated(): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/hot") +@requires_basic_auth_if_no_ano +def feed_hot(): + off = request.args.get("offset") or 0 + all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by( + ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) + hot_books = all_books.offset(off).limit(config.config_books_per_page) + entries = list() + for book in hot_books: + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first() + if downloadBook: + entries.append( + db.session.query(db.Books).filter(common_filters()) + .filter(db.Books.id == book.Downloads.book_id).first() + ) + else: + ub.delete_download(book.Downloads.book_id) + # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() + # ub.session.commit() + numBooks = entries.__len__() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), + config.config_books_per_page, numBooks) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/author") +@requires_basic_auth_if_no_ano +def feed_authorindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\ + .group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Authors).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination) + + +@opds.route("/opds/author/") +@requires_basic_auth_if_no_ano +def feed_author(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + 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") +@requires_basic_auth_if_no_ano +def feed_publisherindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\ + .group_by('books_publishers_link.publisher').order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Publishers).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination) + + +@opds.route("/opds/publisher/") +@requires_basic_auth_if_no_ano +def feed_publisher(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + 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") +@requires_basic_auth_if_no_ano +def feed_categoryindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\ + .group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Tags).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination) + + +@opds.route("/opds/category/") +@requires_basic_auth_if_no_ano +def feed_category(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + 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") +@requires_basic_auth_if_no_ano +def feed_seriesindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\ + .group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Series).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination) + + +@opds.route("/opds/series/") +@requires_basic_auth_if_no_ano +def feed_series(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/shelfindex/", defaults={'public': 0}) +@opds.route("/opds/shelfindex/") +@requires_basic_auth_if_no_ano +def feed_shelfindex(public): + off = request.args.get("offset") or 0 + if public is not 0: + shelf = g.public_shelfes + number = len(shelf) + else: + shelf = g.user.shelf + number = shelf.count() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + number) + return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination) + + +@opds.route("/opds/shelf/") +@requires_basic_auth_if_no_ano +def feed_shelf(book_id): + off = request.args.get("offset") or 0 + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == book_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == book_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + result.append(cur_book) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(result)) + return render_xml_template('feed.xml', entries=result, pagination=pagination) + + +@opds.route("/opds/download///") +@requires_basic_auth_if_no_ano +# @download_required +def get_opds_download_link(book_id, book_format): + book_format = book_format.split(".")[0] + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first() + app.logger.info(data.name) + if current_user.is_authenticated: + ub.update_download(book_id, int(current_user.id)) + file_name = book.title + if len(book.authors) > 0: + file_name = book.authors[0].name + '_' + file_name + file_name = helper.get_valid_filename(file_name) + headers = Headers() + headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf8')), + book_format) + try: + headers["Content-Type"] = mimetypes.types_map['.' + book_format] + except KeyError: + headers["Content-Type"] = "application/octet-stream" + return helper.do_download_file(book, book_format, data, headers) + +@opds.route("/ajax/book/") +@requires_basic_auth_if_no_ano +def get_metadata_calibre_companion(uuid): + entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() + if entry is not None: + js = render_template('json.txt', entry=entry) + response = make_response(js) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response + else: + return "" + + +def feed_search(term): + if term: + term = term.strip().lower() + entries = get_search_results( term) + entriescount = len(entries) if len(entries) > 0 else 1 + pagination = Pagination(1, entriescount, entriescount) + return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) + else: + return render_xml_template('feed.xml', searchterm="") + +def check_auth(username, password): + user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first() + return bool(user and check_password_hash(user.password, password)) + + +def authenticate(): + return Response( + 'Could not verify your access level for that URL.\n' + 'You have to login with proper credentials', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + + +def render_xml_template(*args, **kwargs): + #ToDo: return time in current timezone similar to %z + currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") + xml = render_template(current_time=currtime, *args, **kwargs) + response = make_response(xml) + response.headers["Content-Type"] = "application/atom+xml; charset=utf-8" + return response + +@opds.route("/opds/thumb_240_240/") +@opds.route("/opds/cover_240_240/") +@opds.route("/opds/cover_90_90/") +@opds.route("/opds/cover/") +@requires_basic_auth_if_no_ano +def feed_get_cover(book_id): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + return helper.get_book_cover(book.path) + +@opds.route("/opds/readbooks/") +@login_required_if_no_ano +def feed_read_books(): + off = request.args.get("offset") or 0 + return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) + + +@opds.route("/opds/unreadbooks/") +@login_required_if_no_ano +def feed_unread_books(): + off = request.args.get("offset") or 0 + return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) diff --git a/cps/pagination.py b/cps/pagination.py index 891d616d..50fbc4e5 100644 --- a/cps/pagination.py +++ b/cps/pagination.py @@ -23,6 +23,7 @@ from math import ceil + # simple pagination for the feed class Pagination(object): def __init__(self, page, per_page, total_count): diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 5e2ce271..f984d10b 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + class ReverseProxied(object): """Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind diff --git a/cps/server.py b/cps/server.py index a2c122dc..016689cc 100644 --- a/cps/server.py +++ b/cps/server.py @@ -37,33 +37,34 @@ except ImportError: gevent_present = False - class server: wsgiserver = None - restart= False + restart = False app = None def __init__(self): signal.signal(signal.SIGINT, self.killServer) signal.signal(signal.SIGTERM, self.killServer) - def init_app(self,application): + def init_app(self, application): self.app = application def start_gevent(self): + ssl_args = dict() try: - ssl_args = dict() - certfile_path = config.get_config_certfile() - keyfile_path = config.get_config_keyfile() + certfile_path = config.get_config_certfile() + keyfile_path = config.get_config_keyfile() if certfile_path and keyfile_path: if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): ssl_args = {"certfile": certfile_path, "keyfile": keyfile_path} else: - self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) + self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem ' + 'to be broken. Ignoring ssl. Cert path: %s | Key path: ' + '%s' % (certfile_path, keyfile_path)) if os.name == 'nt': - self.wsgiserver= WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args) + self.wsgiserver = WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args) else: self.wsgiserver = WSGIServer(('', config.config_port), self.app, spawn=Pool(), **ssl_args) self.wsgiserver.serve_forever() @@ -90,14 +91,16 @@ class server: try: ssl = None self.app.logger.info('Starting Tornado server') - certfile_path = config.get_config_certfile() - keyfile_path = config.get_config_keyfile() + certfile_path = config.get_config_certfile() + keyfile_path = config.get_config_keyfile() if certfile_path and keyfile_path: if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): ssl = {"certfile": certfile_path, "keyfile": keyfile_path} else: - self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) + self.app.logger.info('The specified paths for the ssl certificate file and/or key file ' + 'seem to be broken. Ignoring ssl. Cert path: %s | Key ' + 'path: %s' % (certfile_path, keyfile_path)) # Max Buffersize set to 200MB http_server = HTTPServer(WSGIContainer(self.app), @@ -114,7 +117,7 @@ class server: global_WorkerThread.stop() sys.exit(1) - if self.restart == True: + if self.restart is True: self.app.logger.info("Performing restart of Calibre-Web") global_WorkerThread.stop() if os.name == 'nt': @@ -145,6 +148,6 @@ class server: @staticmethod def getNameVersion(): if gevent_present: - return {'Gevent':'v' + geventVersion} + return {'Gevent': 'v' + geventVersion} else: - return {'Tornado':'v'+tornadoVersion} + return {'Tornado': 'v' + tornadoVersion} diff --git a/cps/shelf.py b/cps/shelf.py new file mode 100644 index 00000000..28ee4a3d --- /dev/null +++ b/cps/shelf.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from flask import Blueprint, request, flash, redirect, url_for +from cps import ub +from flask_babel import gettext as _ +from sqlalchemy.sql.expression import func, or_ +from flask_login import login_required, current_user +from web import render_title_template +from cps import app +import db + +shelf = Blueprint('shelf', __name__) + +@shelf.route("/shelf/add//") +@login_required +def add_to_shelf(shelf_id, book_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + app.logger.info("Invalid shelf specified") + if not request.is_xhr: + flash(_(u"Invalid shelf specified"), category="error") + return redirect(url_for('index')) + return "Invalid shelf specified", 400 + + if not shelf.is_public and not shelf.user_id == int(current_user.id): + app.logger.info("Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name) + if not request.is_xhr: + flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), + category="error") + return redirect(url_for('index')) + return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 + + if shelf.is_public and not current_user.role_edit_shelfs(): + app.logger.info("User is not allowed to edit public shelves") + if not request.is_xhr: + flash(_(u"You are not allowed to edit public shelves"), category="error") + return redirect(url_for('index')) + return "User is not allowed to edit public shelves", 403 + + book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, + ub.BookShelf.book_id == book_id).first() + if book_in_shelf: + app.logger.info("Book is already part of the shelf: %s" % shelf.name) + if not request.is_xhr: + flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") + return redirect(url_for('index')) + return "Book is already part of the shelf: %s" % shelf.name, 400 + + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() + if maxOrder[0] is None: + maxOrder = 0 + else: + maxOrder = maxOrder[0] + + ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) + ub.session.add(ins) + ub.session.commit() + if not request.is_xhr: + flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") + if "HTTP_REFERER" in request.environ: + return redirect(request.environ["HTTP_REFERER"]) + else: + return redirect(url_for('index')) + return "", 204 + + +@shelf.route("/shelf/massadd/") +@login_required +def search_to_shelf(shelf_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + app.logger.info("Invalid shelf specified") + flash(_(u"Invalid shelf specified"), category="error") + return redirect(url_for('index')) + + if not shelf.is_public and not shelf.user_id == int(current_user.id): + app.logger.info("You are not allowed to add a book to the the shelf: %s" % shelf.name) + flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") + return redirect(url_for('index')) + + if shelf.is_public and not current_user.role_edit_shelfs(): + app.logger.info("User is not allowed to edit public shelves") + flash(_(u"User is not allowed to edit public shelves"), category="error") + return redirect(url_for('index')) + + if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]: + books_for_shelf = list() + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() + if books_in_shelf: + book_ids = list() + for book_id in books_in_shelf: + book_ids.append(book_id.book_id) + for id in ub.searched_ids[current_user.id]: + if id not in book_ids: + books_for_shelf.append(id) + else: + books_for_shelf = ub.searched_ids[current_user.id] + + if not books_for_shelf: + app.logger.info("Books are already part of the shelf: %s" % shelf.name) + flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") + return redirect(url_for('index')) + + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() + if maxOrder[0] is None: + maxOrder = 0 + else: + maxOrder = maxOrder[0] + + for book in books_for_shelf: + maxOrder = maxOrder + 1 + ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder) + ub.session.add(ins) + ub.session.commit() + flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success") + else: + flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error") + return redirect(url_for('index')) + + +@shelf.route("/shelf/remove//") +@login_required +def remove_from_shelf(shelf_id, book_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + app.logger.info("Invalid shelf specified") + if not request.is_xhr: + return redirect(url_for('index')) + return "Invalid shelf specified", 400 + + # if shelf is public and use is allowed to edit shelfs, or if shelf is private and user is owner + # allow editing shelfs + # result shelf public user allowed user owner + # false 1 0 x + # true 1 1 x + # true 0 x 1 + # false 0 x 0 + + if (not shelf.is_public and shelf.user_id == int(current_user.id)) \ + or (shelf.is_public and current_user.role_edit_shelfs()): + book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, + ub.BookShelf.book_id == book_id).first() + + if book_shelf is None: + app.logger.info("Book already removed from shelf") + if not request.is_xhr: + return redirect(url_for('index')) + return "Book already removed from shelf", 410 + + ub.session.delete(book_shelf) + ub.session.commit() + + if not request.is_xhr: + flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") + return redirect(request.environ["HTTP_REFERER"]) + return "", 204 + else: + app.logger.info("Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name) + if not request.is_xhr: + flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), + category="error") + return redirect(url_for('index')) + return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403 + + + +@shelf.route("/shelf/create", methods=["GET", "POST"]) +@login_required +def create_shelf(): + shelf = ub.Shelf() + if request.method == "POST": + to_save = request.form.to_dict() + if "is_public" in to_save: + shelf.is_public = 1 + shelf.name = to_save["title"] + shelf.user_id = int(current_user.id) + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first() + if existing_shelf: + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") + else: + try: + ub.session.add(shelf) + ub.session.commit() + flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success") + except Exception: + flash(_(u"There was an error"), category="error") + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") + else: + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") + + +@shelf.route("/shelf/edit/", methods=["GET", "POST"]) +@login_required +def edit_shelf(shelf_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if request.method == "POST": + to_save = request.form.to_dict() + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter( + ub.Shelf.id != shelf_id).first() + if existing_shelf: + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") + else: + shelf.name = to_save["title"] + if "is_public" in to_save: + shelf.is_public = 1 + else: + shelf.is_public = 0 + try: + ub.session.commit() + flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success") + except Exception: + flash(_(u"There was an error"), category="error") + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + else: + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + + +@shelf.route("/shelf/delete/") +@login_required +def delete_shelf(shelf_id): + cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + deleted = None + if current_user.role_admin(): + deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete() + else: + if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \ + or (cur_shelf.is_public and current_user.role_edit_shelfs()): + deleted = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).delete() + + if deleted: + ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() + ub.session.commit() + app.logger.info(_(u"successfully deleted shelf %(name)s", name=cur_shelf.name, category="success")) + return redirect(url_for('index')) + + +@shelf.route("/shelf/") +@login_required +def show_shelf(shelf_id): + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if cur_book: + result.append(cur_book) + else: + app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() + ub.session.commit() + return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("web.index")) + + +@shelf.route("/shelfdown/") +@login_required +def show_shelf_down(shelf_id): + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if cur_book: + result.append(cur_book) + else: + app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() + ub.session.commit() + return render_title_template('shelfdown.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("web.index")) + +@shelf.route("/shelf/order/", methods=["GET", "POST"]) +@login_required +def order_shelf(shelf_id): + if request.method == "POST": + to_save = request.form.to_dict() + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + counter = 0 + for book in books_in_shelf: + setattr(book, 'order', to_save[str(book.book_id)]) + counter += 1 + ub.session.commit() + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + if shelf: + books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ + .order_by(ub.BookShelf.order.asc()).all() + for book in books_in_shelf2: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + result.append(cur_book) + return render_title_template('shelf_order.html', entries=result, + title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelforder") diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 4063f23b..4984a192 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -18,7 +18,7 @@ {% for user in content %} {% if not user.role_anonymous() or config.config_anonbrowse %} - {{user.nickname}} + {{user.nickname}} {{user.email}} {{user.kindle_mail}} {{user.downloads.count()}} @@ -30,7 +30,7 @@ {% endif %} {% endfor %} - + @@ -53,7 +53,7 @@ {{email.mail_from}} - + @@ -96,8 +96,8 @@
{% if config.config_remote_login %}{% else %}{% endif %}
- - + + diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 3d885c32..6abe9685 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -267,10 +267,10 @@
{% if not origin %} - {{_('Back')}} + {{_('Back')}} {% endif %} {% if success %} - {{_('Login')}} + {{_('Login')}} {% endif %}
diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index 3b8ebf80..86df4451 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -172,7 +172,7 @@
- {{_('Back')}} + {{_('Back')}}
diff --git a/cps/templates/email_edit.html b/cps/templates/email_edit.html index 0af404b8..00b60640 100644 --- a/cps/templates/email_edit.html +++ b/cps/templates/email_edit.html @@ -37,7 +37,7 @@ - {{_('Back')}} + {{_('Back')}} {% if g.allow_registration %}

{{_('Allowed domains for registering')}}

diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 540f57ed..f848720c 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -6,10 +6,10 @@ href="{{request.script_root + request.full_path}}" type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> {% if pagination.has_prev %} {% endif %} - + {{instance}} {{instance}} @@ -61,11 +61,11 @@ {% endfor %} {% if entry.comments[0] %}{{entry.comments[0].text|striptags}}{% endif %} {% if entry.has_cover %} - - + + {% endif %} {% for format in entry.data %} - {% endfor %} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index cc55cce5..438e713b 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -71,7 +71,7 @@
  • {% endif %} {% if g.user.role_admin() %} -
  • +
  • {% endif %}
  • {% if not g.user.is_anonymous %} @@ -172,11 +172,11 @@ {% endfor %} {% for shelf in g.user.shelf %} -
  • {{shelf.name|shortentitle(40)}}
  • +
  • {{shelf.name|shortentitle(40)}}
  • {% endfor %} {% if not g.user.is_anonymous %} - - + + {% endif %} {% endif %} diff --git a/cps/templates/search.html b/cps/templates/search.html index d8fde51a..9c15b199 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -35,18 +35,18 @@
    {% if entry.has_cover is defined %} - - {{ entry.title }} + + {{ entry.title }} {% endif %}
    - +

    {{entry.title|shortentitle}}

    {% for author in entry.authors %} - {{author.name.replace('|',',')}} + {{author.name.replace('|',',')}} {% if not loop.last %} & {% endif %} diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index cfeb4eee..d0f4ee67 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -3,13 +3,13 @@

    {{title}}

    {% if g.user.role_download() %} - {{ _('Download') }} + {{ _('Download') }} {% endif %} {% if g.user.is_authenticated %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
    {{ _('Delete this Shelf') }}
    - {{ _('Edit Shelf') }} - {{ _('Change order') }} + {{ _('Edit Shelf') }} + {{ _('Change order') }} {% endif %} {% endif %}
    @@ -17,21 +17,21 @@ {% for entry in entries %}
    - +

    {{entry.title|shortentitle}}

    {% for author in entry.authors %} - {{author.name.replace('|',',')}} + {{author.name.replace('|',',')}} {% if not loop.last %} & {% endif %} @@ -63,7 +63,7 @@

    diff --git a/cps/templates/shelf_edit.html b/cps/templates/shelf_edit.html index e9bc1523..d7f32dc4 100644 --- a/cps/templates/shelf_edit.html +++ b/cps/templates/shelf_edit.html @@ -16,7 +16,7 @@ {% endif %} {% if shelf.id != None %} - {{_('Back')}} + {{_('Back')}} {% endif %}
    diff --git a/cps/ub.py b/cps/ub.py index 177f9ce9..ba69c4ec 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -35,6 +35,8 @@ import cli engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False) Base = declarative_base() +session = None + ROLE_USER = 0 ROLE_ADMIN = 1 ROLE_DOWNLOAD = 2 @@ -849,22 +851,23 @@ def create_admin_user(): except Exception: session.rollback() - -# Open session for database connection -Session = sessionmaker() -Session.configure(bind=engine) -session = Session() +def init_db(): + # Open session for database connection + global session + Session = sessionmaker() + Session.configure(bind=engine) + session = Session() -if not os.path.exists(cli.settingspath): - try: + if not os.path.exists(cli.settingspath): + try: + Base.metadata.create_all(engine) + create_default_config() + create_admin_user() + create_anonymous_user() + except Exception: + raise + else: Base.metadata.create_all(engine) - create_default_config() - create_admin_user() - create_anonymous_user() - except Exception: - raise -else: - Base.metadata.create_all(engine) - migrate_Database() - clean_database() + migrate_Database() + clean_database() diff --git a/cps/updater.py b/cps/updater.py index 91d86660..b01646a0 100644 --- a/cps/updater.py +++ b/cps/updater.py @@ -17,26 +17,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + +from cps import config, get_locale import threading import zipfile import requests -import re import logging -import server import time from io import BytesIO import os import sys import shutil -from cps import config from ub import UPDATE_STABLE from tempfile import gettempdir import datetime import json from flask_babel import gettext as _ from babel.dates import format_datetime -import web +import server def is_sha1(sha1): if len(sha1) != 40: @@ -288,7 +287,7 @@ class Updater(threading.Thread): update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parents.append( [ - format_datetime(new_commit_date, format='short', locale=web.get_locale()), + format_datetime(new_commit_date, format='short', locale=get_locale()), update_data['message'], update_data['sha'] ] @@ -318,7 +317,7 @@ class Updater(threading.Thread): parent_commit_date = datetime.datetime.strptime( parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parent_commit_date = format_datetime( - parent_commit_date, format='short', locale=web.get_locale()) + parent_commit_date, format='short', locale=get_locale()) parents.append([parent_commit_date, parent_data['message'].replace('\r\n','

    ').replace('\n','

    ')]) @@ -346,7 +345,7 @@ class Updater(threading.Thread): commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parents.append( [ - format_datetime(new_commit_date, format='short', locale=web.get_locale()), + format_datetime(new_commit_date, format='short', locale=get_locale()), commit['message'], commit['sha'] ] @@ -376,7 +375,7 @@ class Updater(threading.Thread): parent_commit_date = datetime.datetime.strptime( parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parent_commit_date = format_datetime( - parent_commit_date, format='short', locale=web.get_locale()) + parent_commit_date, format='short', locale=get_locale()) parents.append([parent_commit_date, parent_data['message'], parent_data['sha']]) parent_commit = parent_data['parents'][0] @@ -510,6 +509,3 @@ class Updater(threading.Thread): status['message'] = _(u'General error') return status, commit - - -updater_thread = Updater() diff --git a/cps/uploader.py b/cps/uploader.py index 8d9b74a4..df516d24 100644 --- a/cps/uploader.py +++ b/cps/uploader.py @@ -17,11 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os +# import os from tempfile import gettempdir import hashlib from collections import namedtuple -import book_formats +import logging +import os +from flask_babel import gettext as _ +import comic BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages') @@ -29,6 +32,158 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d :rtype: BookMeta """ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski 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 . + + + +try: + from lxml.etree import LXML_VERSION as lxmlversion +except ImportError: + lxmlversion = None + +__author__ = 'lemmsh' + +logger = logging.getLogger("book_formats") + +try: + from wand.image import Image + from wand import version as ImageVersion + use_generic_pdf_cover = False +except (ImportError, RuntimeError) as e: + logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) + use_generic_pdf_cover = True +try: + from PyPDF2 import PdfFileReader + from PyPDF2 import __version__ as PyPdfVersion + use_pdf_meta = True +except ImportError as e: + logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e) + use_pdf_meta = False + +try: + import epub + use_epub_meta = True +except ImportError as e: + logger.warning('cannot import epub, extracting epub metadata will not work: %s', e) + use_epub_meta = False + +try: + import fb2 + use_fb2_meta = True +except ImportError as e: + logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) + use_fb2_meta = False + + +def process(tmp_file_path, original_file_name, original_file_extension): + meta = None + try: + if ".PDF" == original_file_extension.upper(): + meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension) + if ".EPUB" == original_file_extension.upper() and use_epub_meta is True: + meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension) + if ".FB2" == original_file_extension.upper() and use_fb2_meta is True: + meta = fb2.get_fb2_info(tmp_file_path, original_file_extension) + if original_file_extension.upper() in ['.CBZ', '.CBT']: + meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension) + + except Exception as ex: + logger.warning('cannot parse metadata, using default: %s', ex) + + if meta and meta.title.strip() and meta.author.strip(): + return meta + else: + return default_meta(tmp_file_path, original_file_name, original_file_extension) + + +def default_meta(tmp_file_path, original_file_name, original_file_extension): + return BookMeta( + file_path=tmp_file_path, + extension=original_file_extension, + title=original_file_name, + author=u"Unknown", + cover=None, + description="", + tags="", + series="", + series_id="", + languages="") + + +def pdf_meta(tmp_file_path, original_file_name, original_file_extension): + + if use_pdf_meta: + pdf = PdfFileReader(open(tmp_file_path, 'rb')) + doc_info = pdf.getDocumentInfo() + else: + doc_info = None + + if doc_info is not None: + author = doc_info.author if doc_info.author else u"Unknown" + title = doc_info.title if doc_info.title else original_file_name + subject = doc_info.subject + else: + author = u"Unknown" + title = original_file_name + subject = "" + return BookMeta( + file_path=tmp_file_path, + extension=original_file_extension, + title=title, + author=author, + cover=pdf_preview(tmp_file_path, original_file_name), + description=subject, + tags="", + series="", + series_id="", + languages="") + + +def pdf_preview(tmp_file_path, tmp_dir): + if use_generic_pdf_cover: + return None + else: + cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" + with Image(filename=tmp_file_path + "[0]", resolution=150) as img: + img.compression_quality = 88 + img.save(filename=os.path.join(tmp_dir, cover_file_name)) + return cover_file_name + + +def get_versions(): + if not use_generic_pdf_cover: + IVersion = ImageVersion.MAGICK_VERSION + WVersion = ImageVersion.VERSION + else: + IVersion = _(u'not installed') + WVersion = _(u'not installed') + if use_pdf_meta: + PVersion='v'+PyPdfVersion + else: + PVersion=_(u'not installed') + if lxmlversion: + XVersion = 'v'+'.'.join(map(str, lxmlversion)) + else: + XVersion = _(u'not installed') + return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion} + def upload(uploadfile): tmp_dir = os.path.join(gettempdir(), 'calibre_web') diff --git a/cps/web.py b/cps/web.py index 36728b7c..346cb005 100644 --- a/cps/web.py +++ b/cps/web.py @@ -21,33 +21,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import mimetypes -import logging -from flask import (Flask, session, render_template, request, Response, redirect, - url_for, send_from_directory, make_response, g, flash, - abort, Markup) -from flask import __version__ as flaskVersion -from werkzeug import __version__ as werkzeugVersion +from cps import mimetypes, global_WorkerThread, searched_ids +from flask import render_template, request, redirect, url_for, send_from_directory, make_response, g, flash, abort # from werkzeug.exceptions import default_exceptions - -from jinja2 import __version__ as jinja2Version import helper import os # from sqlalchemy.sql.expression import func # from sqlalchemy.sql.expression import false from sqlalchemy.exc import IntegrityError -from sqlalchemy import __version__ as sqlalchemyVersion -from flask_login import (login_user, logout_user, - login_required, current_user) -from flask_principal import __version__ as flask_principalVersion +from flask_login import login_user, logout_user, login_required, current_user from flask_babel import gettext as _ -import requests + from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.datastructures import Headers from babel import Locale as LC -from babel import negotiate_locale -from babel import __version__ as babelVersion -from babel.dates import format_date, format_datetime +from babel.dates import format_date from babel.core import UnknownLocaleError from functools import wraps import base64 @@ -55,30 +43,14 @@ from sqlalchemy.sql import * import json import datetime from iso639 import languages as isoLanguages -from iso639 import __version__ as iso639Version -from pytz import __version__ as pytzVersion -from uuid import uuid4 import os.path -import sys import re import db -from shutil import move, copyfile import gdriveutils -import converter -import tempfile from redirect import redirect_back -import time -import server -from updater import updater_thread -#from flask_dance.contrib.github import make_github_blueprint, github -#from flask_dance.contrib.google import make_google_blueprint, google -#from flask_dance.consumer import oauth_authorized, oauth_error -#from sqlalchemy.orm.exc import NoResultFound -# from oauth import OAuthBackend -import hashlib -from cps import lm, babel, ub_session, config, Server -import ub +from cps import lm, babel, ub, config, get_locale, language_table, app from pagination import Pagination +# from oauth_bb import oauth_check, register_user_with_oauth try: from googleapiclient.errors import HttpError @@ -111,12 +83,7 @@ except ImportError: try: from natsort import natsorted as sort except ImportError: - sort=sorted # Just use regular sort then - # may cause issues with badly named pages in cbz/cbr files -try: - import cPickle -except ImportError: - import pickle as cPickle + sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files try: from urllib.parse import quote @@ -124,48 +91,15 @@ try: except ImportError: from urllib import quote -try: - from flask_login import __version__ as flask_loginVersion -except ImportError: - from flask_login.__about__ import __version__ as flask_loginVersion +from flask import Blueprint # Global variables -current_milli_time = lambda: int(round(time.time() * 1000)) -gdrive_watch_callback_token = 'target=calibreweb-watch_files' -# ToDo: Somehow caused by circular import under python3 refactor -EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', - 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} -EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'} + EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} # EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) -oauth_check = {} - - - -# Main code -mimetypes.init() -mimetypes.add_type('application/xhtml+xml', '.xhtml') -mimetypes.add_type('application/epub+zip', '.epub') -mimetypes.add_type('application/fb2+zip', '.fb2') -mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') -mimetypes.add_type('application/x-mobipocket-ebook', '.prc') -mimetypes.add_type('application/vnd.amazon.ebook', '.azw') -mimetypes.add_type('application/x-cbr', '.cbr') -mimetypes.add_type('application/x-cbz', '.cbz') -mimetypes.add_type('application/x-cbt', '.cbt') -mimetypes.add_type('image/vnd.djvu', '.djvu') -mimetypes.add_type('application/mpeg', '.mpeg') -mimetypes.add_type('application/mpeg', '.mp3') -mimetypes.add_type('application/mp4', '.m4a') -mimetypes.add_type('application/mp4', '.m4b') -mimetypes.add_type('application/ogg', '.ogg') -mimetypes.add_type('application/ogg', '.oga') - - -app = (Flask(__name__)) ''''# custom error page def error_http(error): @@ -181,55 +115,19 @@ for ex in default_exceptions: if ex < 500: app.register_error_handler(ex, error_http) - - -# import uploader - ''' -from flask import Blueprint - web = Blueprint('web', __name__) -def is_gdrive_ready(): - return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \ - os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials')) - - - -@babel.localeselector -def get_locale(): - # if a user is logged in, use the locale from the user settings - user = getattr(g, 'user', None) - if user is not None and hasattr(user, "locale"): - if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings - return user.locale - translations = [str(item) for item in babel.list_translations()] + ['en'] - preferred = list() - for x in request.accept_languages.values(): - try: - preferred.append(str(LC.parse(x.replace('-', '_')))) - except (UnknownLocaleError, ValueError) as e: - app.logger.debug("Could not parse locale: %s", e) - preferred.append('en') - return negotiate_locale(preferred, translations) - - -@babel.timezoneselector -def get_timezone(): - user = getattr(g, 'user', None) - if user is not None: - return user.timezone - - @lm.user_loader def load_user(user_id): try: - return ub_session.query(ub.User).filter(ub.User.id == int(user_id)).first() + return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() except Exception as e: print(e) + @lm.header_loader def load_user_from_header(header_val): if header_val.startswith('Basic '): @@ -247,30 +145,6 @@ def load_user_from_header(header_val): return -def check_auth(username, password): - user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first() - return bool(user and check_password_hash(user.password, password)) - - -def authenticate(): - return Response( - 'Could not verify your access level for that URL.\n' - 'You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) - - -def requires_basic_auth_if_no_ano(f): - @wraps(f) - def decorated(*args, **kwargs): - auth = request.authorization - if config.config_anonbrowse != 1: - if not auth or not check_auth(auth.username, auth.password): - return authenticate() - return f(*args, **kwargs) - - return decorated - - def login_required_if_no_ano(func): @wraps(func) def decorated_view(*args, **kwargs): @@ -324,85 +198,6 @@ def google_oauth_required(f): return inner -# custom jinja filters - -# pagination links in jinja -@web.app_template_filter('url_for_other_page') -def url_for_other_page(page): - args = request.view_args.copy() - args['page'] = page - return url_for(request.endpoint, **args) - - -# shortentitles to at longest nchar, shorten longer words if necessary -@web.app_template_filter('shortentitle') -def shortentitle_filter(s, nchar=20): - text = s.split() - res = "" # result - suml = 0 # overall length - for line in text: - if suml >= 60: - res += '...' - break - # if word longer than 20 chars truncate line and append '...', otherwise add whole word to result - # string, and summarize total length to stop at chars given by nchar - if len(line) > nchar: - res += line[:(nchar-3)] + '[..] ' - suml += nchar+3 - else: - res += line + ' ' - suml += len(line) + 1 - return res.strip() - - -@web.app_template_filter('mimetype') -def mimetype_filter(val): - try: - s = mimetypes.types_map['.' + val] - except Exception: - s = 'application/octet-stream' - return s - - -@web.app_template_filter('formatdate') -def formatdate_filter(val): - conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) - formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") - return format_date(formatdate, format='medium', locale=get_locale()) - - -@web.app_template_filter('formatdateinput') -def format_date_input(val): - conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) - date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") - input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900 - return '' if input_date == "0101-01-01" else input_date - - -@web.app_template_filter('strftime') -def timestamptodate(date, fmt=None): - date = datetime.datetime.fromtimestamp( - int(date)/1000 - ) - native = date.replace(tzinfo=None) - if fmt: - time_format = fmt - else: - time_format = '%d %m %Y - %H:%S' - return native.strftime(time_format) - - -@web.app_template_filter('yesno') -def yesno(value, yes, no): - return yes if value else no - - -'''@web.app_template_filter('canread') -def canread(ext): - if isinstance(ext, db.Data): - ext = ext.format - return ext.lower() in EXTENSIONS_READER''' - def admin_required(f): """ @@ -485,6 +280,7 @@ def speaking_language(languages=None): lang.name = _(isoLanguages.get(part3=lang.lang_code).name) return languages + # Orders all Authors in the list according to authors sort def order_authors(entry): sort_authors = entry.author_sort.split('&') @@ -501,6 +297,7 @@ def order_authors(entry): entry.authors = authors_ordered return entry + # Fill indexpage with all requested data from database def fill_indexpage(page, database, db_filter, order, *join): if current_user.show_detail_random(): @@ -510,121 +307,14 @@ def fill_indexpage(page, database, db_filter, order, *join): randm = false() off = int(int(config.config_books_per_page) * (page - 1)) pagination = Pagination(page, config.config_books_per_page, - len(db.session.query(database) - .filter(db_filter).filter(common_filters()).all())) - entries = db.session.query(database).join(*join,isouter=True).filter(db_filter)\ - .filter(common_filters()).order_by(*order).offset(off).limit(config.config_books_per_page).all() + len(db.session.query(database).filter(db_filter).filter(common_filters()).all())) + entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\ + order_by(*order).offset(off).limit(config.config_books_per_page).all() for book in entries: book = order_authors(book) return entries, randm, pagination -# Modifies different Database objects, first check if elements have to be added to database, than check -# if elements have to be deleted, because they are no longer used -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 - if not isinstance(input_elements, list): - raise TypeError(str(input_elements) + " should be passed as a list") - - input_elements = [x for x in input_elements if x != ''] - # we have all input element (authors, series, tags) names now - # 1. search for elements to remove - del_elements = [] - for c_elements in db_book_object: - found = False - if db_type == 'languages': - type_elements = c_elements.lang_code - elif db_type == 'custom': - type_elements = c_elements.value - else: - type_elements = c_elements.name - for inp_element in input_elements: - if inp_element.lower() == type_elements.lower(): - # if inp_element == type_elements: - found = True - break - # if the element was not found in the new list, add it to remove list - if not found: - del_elements.append(c_elements) - # 2. search for elements that need to be added - add_elements = [] - for inp_element in input_elements: - found = False - for c_elements in db_book_object: - if db_type == 'languages': - type_elements = c_elements.lang_code - elif db_type == 'custom': - type_elements = c_elements.value - else: - type_elements = c_elements.name - if inp_element == type_elements: - found = True - break - if not found: - add_elements.append(inp_element) - # if there are elements to remove, we remove them now - if len(del_elements) > 0: - for del_element in del_elements: - db_book_object.remove(del_element) - if len(del_element.books) == 0: - db_session.delete(del_element) - # if there are elements to add, we add them now! - if len(add_elements) > 0: - if db_type == 'languages': - db_filter = db_object.lang_code - elif db_type == 'custom': - db_filter = db_object.value - else: - db_filter = db_object.name - for add_element in add_elements: - # check if a element with that name exists - db_element = db_session.query(db_object).filter(db_filter == add_element).first() - # if no element is found add it - # if new_element is None: - if db_type == 'author': - new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") - elif db_type == 'series': - new_element = db_object(add_element, add_element) - elif db_type == 'custom': - new_element = db_object(value=add_element) - elif db_type == 'publisher': - new_element = db_object(add_element, None) - else: # db_type should be tag or language - new_element = db_object(add_element) - if db_element is None: - db_session.add(new_element) - db_book_object.append(new_element) - else: - if db_type == 'custom': - if db_element.value != add_element: - new_element.value = add_element - # new_element = db_element - elif db_type == 'languages': - if db_element.lang_code != add_element: - db_element.lang_code = add_element - # new_element = db_element - elif db_type == 'series': - if db_element.name != add_element: - db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element) - db_element.sort = add_element - # new_element = db_element - elif db_type == 'author': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = add_element.replace('|', ',') - # new_element = db_element - elif db_type == 'publisher': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = None - # new_element = db_element - elif db_element.name != add_element: - db_element.name = add_element - # new_element = db_element - # add element to book - db_book_object.append(db_element) - - # read search results from calibre-database and return it (function is used for feed and simple search def get_search_results(term): q = list() @@ -642,32 +332,12 @@ def get_search_results(term): db.Books.title.ilike("%" + term + "%"))).all() -def feed_search(term): - if term: - term = term.strip().lower() - entries = get_search_results( term) - entriescount = len(entries) if len(entries) > 0 else 1 - pagination = Pagination(1, entriescount, entriescount) - return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) - else: - return render_xml_template('feed.xml', searchterm="") - - -def render_xml_template(*args, **kwargs): - #ToDo: return time in current timezone similar to %z - currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") - xml = render_template(current_time=currtime, *args, **kwargs) - response = make_response(xml) - response.headers["Content-Type"] = "application/atom+xml; charset=utf-8" - return response - - -# Returns the template for redering and includes the instance name +# Returns the template for rendering and includes the instance name def render_title_template(*args, **kwargs): return render_template(instance=config.config_calibre_web_title, *args, **kwargs) -@web.before_request +@web.before_app_request def before_request(): g.user = current_user g.allow_registration = config.config_public_reg @@ -678,247 +348,12 @@ def before_request(): return redirect(url_for('web.basic_configuration')) -# Routing functions - -@web.route("/opds") -@requires_basic_auth_if_no_ano -def feed_index(): - return render_xml_template('index.xml') - - -@web.route("/opds/osd") -@requires_basic_auth_if_no_ano -def feed_osd(): - return render_xml_template('osd.xml', lang='en-EN') - - -@web.route("/opds/search/") -@requires_basic_auth_if_no_ano -def feed_cc_search(query): - return feed_search(query.strip()) - - -@web.route("/opds/search", methods=["GET"]) -@requires_basic_auth_if_no_ano -def feed_normal_search(): - return feed_search(request.args.get("query").strip()) - - -@web.route("/opds/new") -@requires_basic_auth_if_no_ano -def feed_new(): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - db.Books, True, [db.Books.timestamp.desc()]) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) - - -@web.route("/opds/discover") -@requires_basic_auth_if_no_ano -def feed_discover(): - entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\ - .limit(config.config_books_per_page) - pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) - - -@web.route("/opds/rated") -@requires_basic_auth_if_no_ano -def feed_best_rated(): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()]) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) - - -@web.route("/opds/hot") -@requires_basic_auth_if_no_ano -def feed_hot(): - off = request.args.get("offset") or 0 - all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by( - ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) - hot_books = all_books.offset(off).limit(config.config_books_per_page) - entries = list() - for book in hot_books: - downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first() - if downloadBook: - entries.append( - db.session.query(db.Books).filter(common_filters()) - .filter(db.Books.id == book.Downloads.book_id).first() - ) - else: - ub.delete_download(book.Downloads.book_id) - # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() - # ub.session.commit() - numBooks = entries.__len__() - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), - config.config_books_per_page, numBooks) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) - - -@web.route("/opds/author") -@requires_basic_auth_if_no_ano -def feed_authorindex(): - off = request.args.get("offset") or 0 - entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\ - .group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Authors).all())) - return render_xml_template('feed.xml', listelements=entries, folder='feed_author', pagination=pagination) - - -@web.route("/opds/author/") -@requires_basic_auth_if_no_ano -def feed_author(book_id): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - 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) - - -@web.route("/opds/publisher") -@requires_basic_auth_if_no_ano -def feed_publisherindex(): - off = request.args.get("offset") or 0 - entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\ - .group_by('books_publishers_link.publisher').order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Publishers).all())) - return render_xml_template('feed.xml', listelements=entries, folder='feed_publisher', pagination=pagination) - - -@web.route("/opds/publisher/") -@requires_basic_auth_if_no_ano -def feed_publisher(book_id): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - 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) - - -@web.route("/opds/category") -@requires_basic_auth_if_no_ano -def feed_categoryindex(): - off = request.args.get("offset") or 0 - entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\ - .group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Tags).all())) - return render_xml_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination) - - -@web.route("/opds/category/") -@requires_basic_auth_if_no_ano -def feed_category(book_id): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - 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) - - -@web.route("/opds/series") -@requires_basic_auth_if_no_ano -def feed_seriesindex(): - off = request.args.get("offset") or 0 - entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\ - .group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all() - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Series).all())) - return render_xml_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination) - - -@web.route("/opds/series/") -@requires_basic_auth_if_no_ano -def feed_series(book_id): - off = request.args.get("offset") or 0 - entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index]) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) - - -@web.route("/opds/shelfindex/", defaults={'public': 0}) -@web.route("/opds/shelfindex/") -@requires_basic_auth_if_no_ano -def feed_shelfindex(public): - off = request.args.get("offset") or 0 - if public is not 0: - shelf = g.public_shelfes - number = len(shelf) - else: - shelf = g.user.shelf - number = shelf.count() - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - number) - return render_xml_template('feed.xml', listelements=shelf, folder='feed_shelf', pagination=pagination) - - -@web.route("/opds/shelf/") -@requires_basic_auth_if_no_ano -def feed_shelf(book_id): - off = request.args.get("offset") or 0 - if current_user.is_anonymous: - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first() - else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == book_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == book_id))).first() - result = list() - # user is allowed to access shelf - if shelf: - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by( - ub.BookShelf.order.asc()).all() - for book in books_in_shelf: - cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() - result.append(cur_book) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(result)) - return render_xml_template('feed.xml', entries=result, pagination=pagination) - - -@web.route("/opds/download///") -@requires_basic_auth_if_no_ano -@download_required -def get_opds_download_link(book_id, book_format): - book_format = book_format.split(".")[0] - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first() - app.logger.info(data.name) - if current_user.is_authenticated: - ub.update_download(book_id, int(current_user.id)) - file_name = book.title - if len(book.authors) > 0: - file_name = book.authors[0].name + '_' + file_name - file_name = helper.get_valid_filename(file_name) - headers = Headers() - headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf8')), - book_format) - try: - headers["Content-Type"] = mimetypes.types_map['.' + book_format] - except KeyError: - headers["Content-Type"] = "application/octet-stream" - return helper.do_download_file(book, book_format, data, headers) - - -@web.route("/ajax/book/") -@requires_basic_auth_if_no_ano -def get_metadata_calibre_companion(uuid): - entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() - if entry is not None: - js = render_template('json.txt', entry=entry) - response = make_response(js) - response.headers["Content-Type"] = "application/json; charset=utf-8" - return response - else: - return "" - @web.route("/ajax/emailstat") @login_required def get_email_status_json(): - tasks=helper.global_WorkerThread.get_taskstatus() + tasks = global_WorkerThread.get_taskstatus() answer = helper.render_task_status(tasks) - js=json.dumps(answer, default=helper.json_serial) + js = json.dumps(answer, default=helper.json_serial) response = make_response(js) response.headers["Content-Type"] = "application/json; charset=utf-8" return response @@ -928,7 +363,7 @@ def get_email_status_json(): # example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; # from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ def check_valid_domain(domain_text): - domain_text = domain_text.split('@',1)[-1].lower() + domain_text = domain_text.split('@', 1)[-1].lower() sql = "SELECT * FROM registration WHERE :domain LIKE domain;" result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() return len(result) @@ -938,14 +373,14 @@ def check_valid_domain(domain_text): @login_required @admin_required def edit_domain(): - ''' POST /post - name: 'username', //name of field (column in db) - pk: 1 //primary key (record id) - value: 'superuser!' //new value''' + # POST /post + # name: 'username', //name of field (column in db) + # pk: 1 //primary key (record id) + # value: 'superuser!' //new value vals = request.form.to_dict() answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() # domain_name = request.args.get('domain') - answer.domain = vals['value'].replace('*','%').replace('?','_').lower() + answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() ub.session.commit() return "" @@ -954,7 +389,7 @@ def edit_domain(): @login_required @admin_required def add_domain(): - domain_name = request.form.to_dict()['domainname'].replace('*','%').replace('?','_').lower() + domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower() check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name).first() if not check: new_domain = ub.Registration(domain=domain_name) @@ -967,7 +402,7 @@ def add_domain(): @login_required @admin_required def delete_domain(): - domain_id = request.form.to_dict()['domainid'].replace('*','%').replace('?','_').lower() + domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() ub.session.commit() # If last domain was deleted, add all domains by default @@ -983,9 +418,9 @@ def delete_domain(): @admin_required def list_domain(): answer = ub.session.query(ub.Registration).all() - json_dumps = json.dumps([{"domain":r.domain.replace('%','*').replace('_','?'),"id":r.id} for r in answer]) - js=json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"') - response = make_response(js.replace("'",'"')) + json_dumps = json.dumps([{"domain": r.domain.replace('%', '*').replace('_', '?'), "id": r.id} for r in answer]) + js = json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"') + response = make_response(js.replace("'", '"')) response.headers["Content-Type"] = "application/json; charset=utf-8" return response @@ -1048,7 +483,7 @@ def get_authors_json(): if request.method == "GET": query = request.args.get('q') entries = db.session.query(db.Authors).filter(db.Authors.name.ilike("%" + query + "%")).all() - json_dumps = json.dumps([dict(name=r.name.replace('|',',')) for r in entries]) + json_dumps = json.dumps([dict(name=r.name.replace('|', ',')) for r in entries]) return json_dumps @@ -1058,7 +493,7 @@ def get_publishers_json(): if request.method == "GET": query = request.args.get('q') entries = db.session.query(db.Publishers).filter(db.Publishers.name.ilike("%" + query + "%")).all() - json_dumps = json.dumps([dict(name=r.name.replace('|',',')) for r in entries]) + json_dumps = json.dumps([dict(name=r.name.replace('|', ',')) for r in entries]) return json_dumps @@ -1081,7 +516,7 @@ def get_languages_json(): languages = language_table[get_locale()] entries_start = [s for key, s in languages.items() if s.lower().startswith(query.lower())] if len(entries_start) < 5: - entries = [s for key,s in languages.items() if query in s.lower()] + entries = [s for key, s in languages.items() if query in s.lower()] entries_start.extend(entries[0:(5-len(entries_start))]) entries_start = list(set(entries_start)) json_dumps = json.dumps([dict(name=r) for r in entries_start[0:5]]) @@ -1125,48 +560,6 @@ def get_matching_tags(): return json_dumps -@web.route("/get_update_status", methods=['GET']) -@login_required_if_no_ano -def get_update_status(): - return updater_thread.get_available_updates(request.method) - - -@web.route("/get_updater_status", methods=['GET', 'POST']) -@login_required -@admin_required -def get_updater_status(): - status = {} - if request.method == "POST": - commit = request.form.to_dict() - if "start" in commit and commit['start'] == 'True': - text = { - "1": _(u'Requesting update package'), - "2": _(u'Downloading update package'), - "3": _(u'Unzipping update package'), - "4": _(u'Replacing files'), - "5": _(u'Database connections are closed'), - "6": _(u'Stopping server'), - "7": _(u'Update finished, please press okay and reload page'), - "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), - "9": _(u'Update failed:') + u' ' + _(u'Connection error'), - "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), - "11": _(u'Update failed:') + u' ' + _(u'General error') - } - status['text'] = text - # helper.updater_thread = helper.Updater() - updater_thread.start() - status['status'] = updater_thread.get_update_status() - elif request.method == "GET": - try: - status['status'] = updater_thread.get_update_status() - except AttributeError: - # thread is not active, occours after restart on update - status['status'] = 7 - except Exception: - status['status'] = 11 - return json.dumps(status) - - @web.route("/", defaults={'page': 1}) @web.route('/page/') @login_required_if_no_ano @@ -1249,7 +642,7 @@ def hot_books(page): return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Hot Books (most downloaded)"), page="hot") else: - abort(404) + abort(404) @web.route("/rated", defaults={'page': 1}) @@ -1287,7 +680,7 @@ def author_list(): .group_by('books_authors_link.author').order_by(db.Authors.sort).all() for entry in entries: entry.Authors.name = entry.Authors.name.replace('|', ',') - return render_title_template('list.html', entries=entries, folder='author', + return render_title_template('list.html', entries=entries, folder='web.author', title=u"Author list", page="authorlist") else: abort(404) @@ -1298,12 +691,12 @@ def author_list(): @login_required_if_no_ano def author(book_id, page): entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id), - [db.Series.name, db.Books.series_index],db.books_series_link, db.Series) + [db.Series.name, db.Books.series_index], db.books_series_link, db.Series) if entries is None: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("web.index")) - name = (db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name).replace('|', ',') + name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name.replace('|', ',') author_info = None other_books = [] @@ -1327,7 +720,7 @@ def publisher_list(): entries = db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count'))\ .join(db.books_publishers_link).join(db.Books).filter(common_filters())\ .group_by('books_publishers_link.publisher').order_by(db.Publishers.sort).all() - return render_title_template('list.html', entries=entries, folder='publisher', + return render_title_template('list.html', entries=entries, folder='web.publisher', title=_(u"Publisher list"), page="publisherlist") else: abort(404) @@ -1340,10 +733,11 @@ def publisher(book_id, page): publisher = db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first() if publisher: entries, random, pagination = fill_indexpage(page, db.Books, - db.Books.publishers.any(db.Publishers.id == book_id), - (db.Series.name, db.Books.series_index), db.books_series_link, db.Series) + db.Books.publishers.any(db.Publishers.id == book_id), + (db.Series.name, db.Books.series_index), db.books_series_link, + db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher") + title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher") else: abort(404) @@ -1352,15 +746,18 @@ def get_unique_other_books(library_books, author_books): # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates # Note: Not all images will be shown, even though they're available on Goodreads.com. # See https://www.goodreads.com/topic/show/18213769-goodreads-book-images - identifiers = reduce(lambda acc, book: acc + map(lambda identifier: identifier.val, book.identifiers), library_books, []) - other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, author_books) + identifiers = reduce(lambda acc, book: acc + map(lambda identifier: identifier.val, book.identifiers), + library_books, []) + other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, + author_books) # Fuzzy match book titles if levenshtein_support: library_titles = reduce(lambda acc, book: acc + [book.title], library_books, []) other_books = filter(lambda author_book: not filter( lambda library_book: - Levenshtein.ratio(re.sub(r"\(.*\)", "", author_book.title), library_book) > 0.7, # Remove items in parentheses before comparing + # Remove items in parentheses before comparing + Levenshtein.ratio(re.sub(r"\(.*\)", "", author_book.title), library_book) > 0.7, library_titles ), other_books) @@ -1374,7 +771,7 @@ def series_list(): entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\ .join(db.books_series_link).join(db.Books).filter(common_filters())\ .group_by('books_series_link.series').order_by(db.Series.sort).all() - return render_title_template('list.html', entries=entries, folder='series', + return render_title_template('list.html', entries=entries, folder='web.series', title=_(u"Series list"), page="serieslist") else: abort(404) @@ -1387,7 +784,7 @@ def series(book_id, page): name = db.session.query(db.Series).filter(db.Series.id == book_id).first() if name: entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.id == book_id), - [db.Books.series_index]) + [db.Books.series_index]) return render_title_template('index.html', random=random, pagination=pagination, entries=entries, title=_(u"Series: %(serie)s", serie=name.name), page="series") else: @@ -1445,7 +842,7 @@ def category_list(): entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\ .join(db.books_tags_link).join(db.Books).order_by(db.Tags.name).filter(common_filters())\ .group_by('books_tags_link.tag').all() - return render_title_template('list.html', entries=entries, folder='category', + return render_title_template('list.html', entries=entries, folder='web.category', title=_(u"Category list"), page="catlist") else: abort(404) @@ -1458,7 +855,8 @@ def category(book_id, page): name = db.session.query(db.Tags).filter(db.Tags.id == book_id).first() if name: entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.id == book_id), - (db.Series.name, db.Books.series_index),db.books_series_link,db.Series) + (db.Series.name, db.Books.series_index),db.books_series_link, + db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Category: %(name)s", name=name.name), page="category") else: @@ -1499,6 +897,7 @@ def toggle_read(book_id): u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) return "" + @web.route("/book/") @login_required_if_no_ano def show_book(book_id): @@ -1528,13 +927,12 @@ def show_book(book_id): if not current_user.is_anonymous: if not config.config_read_column: - matching_have_read_book = ub.session.query(ub.ReadBook)\ - .filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), - ub.ReadBook.book_id == book_id)).all() + matching_have_read_book = ub.session.query(ub.ReadBook).\ + filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).all() have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].is_read else: try: - matching_have_read_book = getattr(entries,'custom_column_'+str(config.config_read_column)) + matching_have_read_book = getattr(entries, 'custom_column_'+str(config.config_read_column)) have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].value except KeyError: app.logger.error( @@ -1544,7 +942,7 @@ def show_book(book_id): else: have_read = None - entries.tags = sort(entries.tags, key = lambda tag: tag.name) + entries.tags = sort(entries.tags, key=lambda tag: tag.name) entries = order_authors(entries) @@ -1556,8 +954,8 @@ def show_book(book_id): if media_format.format.lower() in EXTENSIONS_AUDIO: audioentries.append(media_format.format.lower()) - return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, is_xhr=request.is_xhr, - title=entries.title, books_shelfs=book_in_shelfs, + return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, + is_xhr=request.is_xhr, title=entries.title, books_shelfs=book_in_shelfs, have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book") else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") @@ -1576,9 +974,9 @@ def bookmark(book_id, book_format): return "", 204 lbookmark = ub.Bookmark(user_id=current_user.id, - book_id=book_id, - format=book_format, - bookmark_key=bookmark_key) + book_id=book_id, + format=book_format, + bookmark_key=bookmark_key) ub.session.merge(lbookmark) ub.session.commit() return "", 201 @@ -1588,254 +986,13 @@ def bookmark(book_id, book_format): @login_required def get_tasks_status(): # if current user admin, show all email, otherwise only own emails - answer=list() - # UIanswer=list() - tasks=helper.global_WorkerThread.get_taskstatus() - # answer = tasks - + tasks = global_WorkerThread.get_taskstatus() # UIanswer = copy.deepcopy(answer) answer = helper.render_task_status(tasks) # foreach row format row return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks") -@web.route("/admin") -@login_required -def admin_forbidden(): - abort(403) - - -@web.route("/stats") -@login_required -def stats(): - counter = db.session.query(db.Books).count() - authors = db.session.query(db.Authors).count() - categorys = db.session.query(db.Tags).count() - series = db.session.query(db.Series).count() - versions = uploader.book_formats.get_versions() - versions['Babel'] = 'v' + babelVersion - versions['Sqlalchemy'] = 'v' + sqlalchemyVersion - versions['Werkzeug'] = 'v' + werkzeugVersion - versions['Jinja2'] = 'v' + jinja2Version - versions['Flask'] = 'v' + flaskVersion - versions['Flask Login'] = 'v' + flask_loginVersion - versions['Flask Principal'] = 'v' + flask_principalVersion - versions['Iso 639'] = 'v' + iso639Version - versions['pytz'] = 'v' + pytzVersion - - versions['Requests'] = 'v' + requests.__version__ - versions['pySqlite'] = 'v' + db.engine.dialect.dbapi.version - versions['Sqlite'] = 'v' + db.engine.dialect.dbapi.sqlite_version - versions.update(converter.versioncheck()) - versions.update(server.Server.getNameVersion()) - versions['Python'] = sys.version - return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, - categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat") - - -@web.route("/delete//", defaults={'book_format': ""}) -@web.route("/delete///") -@login_required -def delete_book(book_id, book_format): - if current_user.role_delete_books(): - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - if book: - helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) - if not book_format: - # delete book from Shelfs, Downloads, Read list - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() - ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete() - ub.delete_download(book_id) - ub.session.commit() - - # check if only this book links to: - # author, language, series, tags, custom columns - modify_database_object([u''], book.authors, db.Authors, db.session, 'author') - modify_database_object([u''], book.tags, db.Tags, db.session, 'tags') - modify_database_object([u''], book.series, db.Series, db.session, 'series') - modify_database_object([u''], book.languages, db.Languages, db.session, 'languages') - modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers') - - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - for c in cc: - cc_string = "custom_column_" + str(c.id) - if not c.is_multiple: - if len(getattr(book, cc_string)) > 0: - if c.datatype == 'bool' or c.datatype == 'integer': - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - db.session.delete(del_cc) - elif c.datatype == 'rating': - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if len(del_cc.books) == 0: - db.session.delete(del_cc) - else: - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - db.session.delete(del_cc) - else: - modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], - db.session, 'custom') - db.session.query(db.Books).filter(db.Books.id == book_id).delete() - else: - db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete() - db.session.commit() - else: - # book not found - app.logger.info('Book with id "'+str(book_id)+'" could not be deleted') - if book_format: - return redirect(url_for('edit_book', book_id=book_id)) - else: - return redirect(url_for('index')) - - - -@web.route("/gdrive/authenticate") -@login_required -@admin_required -def authenticate_google_drive(): - try: - authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl() - except gdriveutils.InvalidConfigError: - flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'), - category="error") - return redirect(url_for('index')) - return redirect(authUrl) - - -@web.route("/gdrive/callback") -def google_drive_callback(): - auth_code = request.args.get('code') - if not auth_code: - abort(403) - try: - credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code) - with open(os.path.join(config.get_main_dir,'gdrive_credentials'), 'w') as f: - f.write(credentials.to_json()) - except ValueError as error: - app.logger.error(error) - return redirect(url_for('configuration')) - - -@web.route("/gdrive/watch/subscribe") -@login_required -@admin_required -def watch_gdrive(): - if not config.config_google_drive_watch_changes_response: - with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings: - filedata = json.load(settings) - if filedata['web']['redirect_uris'][0].endswith('/'): - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] - else: - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))] - address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0] - notification_id = str(uuid4()) - try: - result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, - 'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000) - settings = ub.session.query(ub.Settings).first() - settings.config_google_drive_watch_changes_response = json.dumps(result) - ub.session.merge(settings) - ub.session.commit() - settings = ub.session.query(ub.Settings).first() - config.loadSettings() - except HttpError as e: - reason=json.loads(e.content)['error']['errors'][0] - if reason['reason'] == u'push.webhookUrlUnauthorized': - flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error") - else: - flash(reason['message'], category="error") - - return redirect(url_for('configuration')) - - -@web.route("/gdrive/watch/revoke") -@login_required -@admin_required -def revoke_watch_gdrive(): - last_watch_response = config.config_google_drive_watch_changes_response - if last_watch_response: - try: - gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], - last_watch_response['resourceId']) - except HttpError: - pass - settings = ub.session.query(ub.Settings).first() - settings.config_google_drive_watch_changes_response = None - ub.session.merge(settings) - ub.session.commit() - config.loadSettings() - return redirect(url_for('configuration')) - - -@web.route("/gdrive/watch/callback", methods=['GET', 'POST']) -def on_received_watch_confirmation(): - app.logger.debug(request.headers) - if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \ - and request.headers.get('X-Goog-Resource-State') == 'change' \ - and request.data: - - data = request.data - - def updateMetaData(): - app.logger.info('Change received from gdrive') - app.logger.debug(data) - try: - j = json.loads(data) - app.logger.info('Getting change details') - response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) - app.logger.debug(response) - if response: - dbpath = os.path.join(config.config_calibre_dir, "metadata.db") - if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath): - tmpDir = tempfile.gettempdir() - app.logger.info('Database file updated') - copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) - app.logger.info('Backing up existing and downloading updated metadata.db') - gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) - app.logger.info('Setting up new DB') - # prevent error on windows, as os.rename does on exisiting files - move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) - db.setup_db() - except Exception as e: - app.logger.info(e.message) - app.logger.exception(e) - updateMetaData() - return '' - - -@web.route("/shutdown") -@login_required -@admin_required -def shutdown(): - task = int(request.args.get("parameter").strip()) - if task == 1 or task == 0: # valid commandos received - # close all database connections - db.session.close() - db.engine.dispose() - ub.session.close() - ub.engine.dispose() - - showtext = {} - if task == 0: - showtext['text'] = _(u'Server restarted, please reload page') - Server.setRestartTyp(True) - else: - showtext['text'] = _(u'Performing shutdown of server, please close window') - Server.setRestartTyp(False) - # stop gevent/tornado server - Server.stopServer() - return json.dumps(showtext) - else: - if task == 2: - db.session.close() - db.engine.dispose() - db.setup_db() - return json.dumps({}) - abort(404) - - @web.route("/search", methods=["GET"]) @login_required_if_no_ano def search(): @@ -1845,7 +1002,7 @@ def search(): ids = list() for element in entries: ids.append(element.id) - ub.searched_ids[current_user.id] = ids + searched_ids[current_user.id] = ids return render_title_template('search.html', searchterm=term, entries=entries, page="search") else: return render_title_template('search.html', searchterm="", page="search") @@ -1884,9 +1041,12 @@ def advanced_search(): rating_low = request.args.get("ratinghigh") rating_high = request.args.get("ratinglow") description = request.args.get("comment") - if author_name: author_name = author_name.strip().lower().replace(',','|') - if book_title: book_title = book_title.strip().lower() - if publisher: publisher = publisher.strip().lower() + if author_name: + author_name = author_name.strip().lower().replace(',','|') + if book_title: + book_title = book_title.strip().lower() + if publisher: + publisher = publisher.strip().lower() searchterm = [] cc_present = False @@ -1899,19 +1059,19 @@ def advanced_search(): include_languages_inputs or exclude_languages_inputs or author_name or book_title or \ publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present: searchterm = [] - searchterm.extend((author_name.replace('|',','), book_title, publisher)) + searchterm.extend((author_name.replace('|', ','), book_title, publisher)) if pub_start: try: searchterm.extend([_(u"Published after ") + - format_date(datetime.datetime.strptime(pub_start,"%Y-%m-%d"), - format='medium', locale=get_locale())]) + format_date(datetime.datetime.strptime(pub_start,"%Y-%m-%d"), + format='medium', locale=get_locale())]) except ValueError: pub_start = u"" if pub_end: try: searchterm.extend([_(u"Published before ") + - format_date(datetime.datetime.strptime(pub_end,"%Y-%m-%d"), - format='medium', locale=get_locale())]) + format_date(datetime.datetime.strptime(pub_end,"%Y-%m-%d"), + format='medium', locale=get_locale())]) except ValueError: pub_start = u"" tag_names = db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all() @@ -1961,7 +1121,7 @@ def advanced_search(): rating_high = int(rating_high) * 2 q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high)) if rating_low: - rating_low = int(rating_low) *2 + rating_low = int(rating_low) * 2 q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low)) if description: q = q.filter(db.Books.comments.any(db.Comments.text.ilike("%" + description + "%"))) @@ -1973,10 +1133,10 @@ def advanced_search(): if c.datatype == 'bool': getattr(db.Books, 'custom_column_1') q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any( - db.cc_classes[c.id].value == (custom_query== "True") )) + db.cc_classes[c.id].value == (custom_query == "True"))) elif c.datatype == 'int': q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any( - db.cc_classes[c.id].value == custom_query )) + db.cc_classes[c.id].value == custom_query)) else: q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any( db.cc_classes[c.id].value.ilike("%" + custom_query + "%"))) @@ -1984,7 +1144,7 @@ def advanced_search(): ids = list() for element in q: ids.append(element.id) - ub.searched_ids[current_user.id] = ids + searched_ids[current_user.id] = ids return render_title_template('search.html', searchterm=searchterm, entries=q, title=_(u"search"), page="search") # prepare data for search-form @@ -2010,7 +1170,8 @@ def get_cover(book_id): def serve_book(book_id, book_format): book_format = book_format.split(".")[0] book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first() + data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper())\ + .first() app.logger.info('Serving book: %s', data.name) if config.config_use_google_drive: headers = Headers() @@ -2024,25 +1185,29 @@ def serve_book(book_id, book_format): return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) -@web.route("/opds/thumb_240_240/") -@web.route("/opds/cover_240_240/") -@web.route("/opds/cover_90_90/") -@web.route("/opds/cover/") -@requires_basic_auth_if_no_ano -def feed_get_cover(book_id): - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - return helper.get_book_cover(book.path) +@web.route("/unreadbooks/", defaults={'page': 1}) +@web.route("/unreadbooks/'") +@login_required_if_no_ano +def unread_books(page): + return render_read_books(page, False) + + +@web.route("/readbooks/", defaults={'page': 1}) +@web.route("/readbooks/'") +@login_required_if_no_ano +def read_books(page): + return render_read_books(page, True) def render_read_books(page, are_read, as_xml=False): if not config.config_read_column: readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\ - .filter(ub.ReadBook.is_read == True).all() + .filter(ub.ReadBook.is_read is True).all() readBookIds = [x.book_id for x in readBooks] else: try: readBooks = db.session.query(db.cc_classes[config.config_read_column])\ - .filter(db.cc_classes[config.config_read_column].value==True).all() + .filter(db.cc_classes[config.config_read_column].value is True).all() readBookIds = [x.book for x in readBooks] except KeyError: app.logger.error(u"Custom Column No.%d is not existing in calibre database" % config.config_read_column) @@ -2053,8 +1218,7 @@ def render_read_books(page, are_read, as_xml=False): else: db_filter = ~db.Books.id.in_(readBookIds) - entries, random, pagination = fill_indexpage(page, db.Books, - db_filter, [db.Books.timestamp.desc()]) + entries, random, pagination = fill_indexpage(page, db.Books, db_filter, [db.Books.timestamp.desc()]) if as_xml: xml = render_title_template('feed.xml', entries=entries, pagination=pagination) @@ -2068,35 +1232,7 @@ def render_read_books(page, are_read, as_xml=False): total_books = db.session.query(func.count(db.Books.id)).scalar() name = _(u'Unread Books') + ' (' + str(total_books - len(readBookIds)) + ')' return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(name, name=name), page="read") - - -@web.route("/opds/readbooks/") -@login_required_if_no_ano -def feed_read_books(): - off = request.args.get("offset") or 0 - return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) - - -@web.route("/readbooks/", defaults={'page': 1}) -@web.route("/readbooks/'") -@login_required_if_no_ano -def read_books(page): - return render_read_books(page, True) - - -@web.route("/opds/unreadbooks/") -@login_required_if_no_ano -def feed_unread_books(): - off = request.args.get("offset") or 0 - return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) - - -@web.route("/unreadbooks/", defaults={'page': 1}) -@web.route("/unreadbooks/'") -@login_required_if_no_ano -def unread_books(page): - return render_read_books(page, False) + title=_(name, name=name), page="read") @web.route("/read//") @@ -2111,8 +1247,8 @@ def read_book(book_id, book_format): bookmark = None if current_user.is_authenticated: bookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), - ub.Bookmark.book_id == book_id, - ub.Bookmark.format == book_format.upper())).first() + ub.Bookmark.book_id == book_id, + ub.Bookmark.format == book_format.upper())).first() if book_format.lower() == "epub": return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark) elif book_format.lower() == "pdf": @@ -2122,24 +1258,24 @@ def read_book(book_id, book_format): elif book_format.lower() == "mp3": entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) elif book_format.lower() == "m4b": entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) elif book_format.lower() == "m4a": entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) else: book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id)) if not os.path.exists(book_dir): os.mkdir(book_dir) for fileext in ["cbr", "cbt", "cbz"]: if book_format.lower() == fileext: - all_name = str(book_id) # + "/" + book.data[0].name + "." + fileext - #tmp_file = os.path.join(book_dir, book.data[0].name) + "." + fileext - #if not os.path.exists(all_name): + all_name = str(book_id) # + "/" + book.data[0].name + "." + fileext + # tmp_file = os.path.join(book_dir, book.data[0].name) + "." + fileext + # if not os.path.exists(all_name): # cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + "." + fileext # copyfile(cbr_file, tmp_file) return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), @@ -2204,7 +1340,8 @@ def register(): flash(_(u"Please fill out all fields!"), category="error") return render_title_template('register.html', title=_(u"register"), page="register") - existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()).first() + existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"] + .lower()).first() existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first() if not existing_user and not existing_email: content = ub.User() @@ -2220,15 +1357,16 @@ def register(): try: ub.session.add(content) ub.session.commit() - register_user_with_oauth(content) - helper.send_registration_mail(to_save["email"],to_save["nickname"], password) + # register_user_with_oauth(content) + helper.send_registration_mail(to_save["email"], to_save["nickname"], password) except Exception: ub.session.rollback() flash(_(u"An unknown error occurred. Please try again later."), category="error") return render_title_template('register.html', title=_(u"register"), page="register") else: flash(_(u"Your e-mail is not allowed to register"), category="error") - app.logger.info('Registering failed for user "' + to_save['nickname'] + '" e-mail adress: ' + to_save["email"]) + app.logger.info('Registering failed for user "' + to_save['nickname'] + '" e-mail adress: ' + + to_save["email"]) return render_title_template('register.html', title=_(u"register"), page="register") flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success") return redirect(url_for('login')) @@ -2236,7 +1374,7 @@ def register(): flash(_(u"This username or e-mail address is already in use."), category="error") return render_title_template('register.html', title=_(u"register"), page="register") - register_user_with_oauth() + # register_user_with_oauth() return render_title_template('register.html', config=config, title=_(u"register"), page="register") @@ -2245,17 +1383,18 @@ def login(): if not config.db_configured: return redirect(url_for('web.basic_configuration')) if current_user is not None and current_user.is_authenticated: - return redirect(url_for('index')) + return redirect(url_for('web.index')) if request.method == "POST": form = request.form.to_dict() - user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()).first() + user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower())\ + .first() if config.config_use_ldap and user: import ldap try: ub.User.try_login(form['username'], form['password']) login_user(user, remember=True) flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") - return redirect_back(url_for("index")) + return redirect_back(url_for("web.index")) except ldap.INVALID_CREDENTIALS: ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) app.logger.info('LDAP Login failed for user "' + form['username'] + '" IP-adress: ' + ipAdress) @@ -2387,325 +1526,6 @@ def send_to_kindle(book_id, book_format, convert): return redirect(request.environ["HTTP_REFERER"]) -@web.route("/shelf/add//") -@login_required -def add_to_shelf(shelf_id, book_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - if shelf is None: - app.logger.info("Invalid shelf specified") - if not request.is_xhr: - flash(_(u"Invalid shelf specified"), category="error") - return redirect(url_for('index')) - return "Invalid shelf specified", 400 - - if not shelf.is_public and not shelf.user_id == int(current_user.id): - app.logger.info("Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name) - if not request.is_xhr: - flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), - category="error") - return redirect(url_for('index')) - return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 - - if shelf.is_public and not current_user.role_edit_shelfs(): - app.logger.info("User is not allowed to edit public shelves") - if not request.is_xhr: - flash(_(u"You are not allowed to edit public shelves"), category="error") - return redirect(url_for('index')) - return "User is not allowed to edit public shelves", 403 - - book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, - ub.BookShelf.book_id == book_id).first() - if book_in_shelf: - app.logger.info("Book is already part of the shelf: %s" % shelf.name) - if not request.is_xhr: - flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") - return redirect(url_for('index')) - return "Book is already part of the shelf: %s" % shelf.name, 400 - - maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() - if maxOrder[0] is None: - maxOrder = 0 - else: - maxOrder = maxOrder[0] - - ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) - ub.session.add(ins) - ub.session.commit() - if not request.is_xhr: - flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") - if "HTTP_REFERER" in request.environ: - return redirect(request.environ["HTTP_REFERER"]) - else: - return redirect(url_for('index')) - return "", 204 - - -@web.route("/shelf/massadd/") -@login_required -def search_to_shelf(shelf_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - if shelf is None: - app.logger.info("Invalid shelf specified") - flash(_(u"Invalid shelf specified"), category="error") - return redirect(url_for('index')) - - if not shelf.is_public and not shelf.user_id == int(current_user.id): - app.logger.info("You are not allowed to add a book to the the shelf: %s" % shelf.name) - flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") - return redirect(url_for('index')) - - if shelf.is_public and not current_user.role_edit_shelfs(): - app.logger.info("User is not allowed to edit public shelves") - flash(_(u"User is not allowed to edit public shelves"), category="error") - return redirect(url_for('index')) - - if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]: - books_for_shelf = list() - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() - if books_in_shelf: - book_ids = list() - for book_id in books_in_shelf: - book_ids.append(book_id.book_id) - for id in ub.searched_ids[current_user.id]: - if id not in book_ids: - books_for_shelf.append(id) - else: - books_for_shelf = ub.searched_ids[current_user.id] - - if not books_for_shelf: - app.logger.info("Books are already part of the shelf: %s" % shelf.name) - flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") - return redirect(url_for('index')) - - maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() - if maxOrder[0] is None: - maxOrder = 0 - else: - maxOrder = maxOrder[0] - - for book in books_for_shelf: - maxOrder = maxOrder + 1 - ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder) - ub.session.add(ins) - ub.session.commit() - flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success") - else: - flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error") - return redirect(url_for('index')) - - -@web.route("/shelf/remove//") -@login_required -def remove_from_shelf(shelf_id, book_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - if shelf is None: - app.logger.info("Invalid shelf specified") - if not request.is_xhr: - return redirect(url_for('index')) - return "Invalid shelf specified", 400 - - # if shelf is public and use is allowed to edit shelfs, or if shelf is private and user is owner - # allow editing shelfs - # result shelf public user allowed user owner - # false 1 0 x - # true 1 1 x - # true 0 x 1 - # false 0 x 0 - - if (not shelf.is_public and shelf.user_id == int(current_user.id)) \ - or (shelf.is_public and current_user.role_edit_shelfs()): - book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, - ub.BookShelf.book_id == book_id).first() - - if book_shelf is None: - app.logger.info("Book already removed from shelf") - if not request.is_xhr: - return redirect(url_for('index')) - return "Book already removed from shelf", 410 - - ub.session.delete(book_shelf) - ub.session.commit() - - if not request.is_xhr: - flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") - return redirect(request.environ["HTTP_REFERER"]) - return "", 204 - else: - app.logger.info("Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name) - if not request.is_xhr: - flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), - category="error") - return redirect(url_for('index')) - return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403 - - - -@web.route("/shelf/create", methods=["GET", "POST"]) -@login_required -def create_shelf(): - shelf = ub.Shelf() - if request.method == "POST": - to_save = request.form.to_dict() - if "is_public" in to_save: - shelf.is_public = 1 - shelf.name = to_save["title"] - shelf.user_id = int(current_user.id) - existing_shelf = ub.session.query(ub.Shelf).filter( - or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), - (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first() - if existing_shelf: - flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") - else: - try: - ub.session.add(shelf) - ub.session.commit() - flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success") - except Exception: - flash(_(u"There was an error"), category="error") - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") - else: - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") - - -@web.route("/shelf/edit/", methods=["GET", "POST"]) -@login_required -def edit_shelf(shelf_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - if request.method == "POST": - to_save = request.form.to_dict() - existing_shelf = ub.session.query(ub.Shelf).filter( - or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), - (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter( - ub.Shelf.id != shelf_id).first() - if existing_shelf: - flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") - else: - shelf.name = to_save["title"] - if "is_public" in to_save: - shelf.is_public = 1 - else: - shelf.is_public = 0 - try: - ub.session.commit() - flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success") - except Exception: - flash(_(u"There was an error"), category="error") - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") - else: - return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") - - -@web.route("/shelf/delete/") -@login_required -def delete_shelf(shelf_id): - cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - deleted = None - if current_user.role_admin(): - deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete() - else: - if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \ - or (cur_shelf.is_public and current_user.role_edit_shelfs()): - deleted = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).delete() - - if deleted: - ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() - ub.session.commit() - app.logger.info(_(u"successfully deleted shelf %(name)s", name=cur_shelf.name, category="success")) - return redirect(url_for('index')) - - -@web.route("/shelf/") -@login_required_if_no_ano -def show_shelf(shelf_id): - if current_user.is_anonymous: - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() - else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).first() - result = list() - # user is allowed to access shelf - if shelf: - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( - ub.BookShelf.order.asc()).all() - for book in books_in_shelf: - cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() - if cur_book: - result.append(cur_book) - else: - app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() - ub.session.commit() - return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), - shelf=shelf, page="shelf") - else: - flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") - return redirect(url_for("web.index")) - - -@web.route("/shelfdown/") -def show_shelf_down(shelf_id): - if current_user.is_anonymous: - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() - else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).first() - result = list() - # user is allowed to access shelf - if shelf: - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( - ub.BookShelf.order.asc()).all() - for book in books_in_shelf: - cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() - if cur_book: - result.append(cur_book) - else: - app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() - ub.session.commit() - return render_title_template('shelfdown.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), - shelf=shelf, page="shelf") - else: - flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") - return redirect(url_for("web.index")) - -@web.route("/shelf/order/", methods=["GET", "POST"]) -@login_required -def order_shelf(shelf_id): - if request.method == "POST": - to_save = request.form.to_dict() - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( - ub.BookShelf.order.asc()).all() - counter = 0 - for book in books_in_shelf: - setattr(book, 'order', to_save[str(book.book_id)]) - counter += 1 - ub.session.commit() - if current_user.is_anonymous: - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() - else: - shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), - ub.Shelf.id == shelf_id), - ub.and_(ub.Shelf.is_public == 1, - ub.Shelf.id == shelf_id))).first() - result = list() - if shelf: - books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ - .order_by(ub.BookShelf.order.asc()).all() - for book in books_in_shelf2: - cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() - result.append(cur_book) - return render_title_template('shelf_order.html', entries=result, - title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), - shelf=shelf, page="shelforder") - - @web.route("/me", methods=["GET", "POST"]) @login_required def profile(): @@ -2713,7 +1533,7 @@ def profile(): downloads = list() languages = speaking_language() translations = babel.list_translations() + [LC('en')] - oauth_status = None # oauth_status = get_oauth_status() + oauth_status = None # oauth_status = get_oauth_status() for book in content.downloads: downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() if downloadBook: @@ -2734,7 +1554,7 @@ def profile(): if config.config_public_reg and not check_valid_domain(to_save["email"]): flash(_(u"E-mail is not from valid domain"), category="error") return render_title_template("user_edit.html", content=content, downloads=downloads, - title=_(u"%(name)s's profile", name=current_user.nickname)) + title=_(u"%(name)s's profile", name=current_user.nickname)) content.email = to_save["email"] if "show_random" in to_save and to_save["show_random"] == "on": content.random_books = 1 @@ -2782,1451 +1602,3 @@ def profile(): content=content, downloads=downloads, title=_(u"%(name)s's profile", name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) - -@web.route("/admin/view") -@login_required -@admin_required -def admin(): - version = updater_thread.get_current_version_info() - if version is False: - commit = _(u'Unknown') - else: - if 'datetime' in version: - commit = version['datetime'] - - tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) - form_date = datetime.datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S") - if len(commit) > 19: # check if string has timezone - if commit[19] == '+': - form_date -= datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) - elif commit[19] == '-': - form_date += datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) - commit = format_datetime(form_date - tz, format='short', locale=get_locale()) - else: - commit = version['version'] - - content = ub.session.query(ub.User).all() - settings = ub.session.query(ub.Settings).first() - return render_title_template("admin.html", content=content, email=settings, config=config, commit=commit, - title=_(u"Admin page"), page="admin") - - -@web.route("/admin/config", methods=["GET", "POST"]) -@login_required -@admin_required -def configuration(): - return configuration_helper(0) - - -@web.route("/admin/viewconfig", methods=["GET", "POST"]) -@login_required -@admin_required -def view_configuration(): - reboot_required = False - if request.method == "POST": - to_save = request.form.to_dict() - content = ub.session.query(ub.Settings).first() - if "config_calibre_web_title" in to_save: - content.config_calibre_web_title = to_save["config_calibre_web_title"] - if "config_columns_to_ignore" in to_save: - content.config_columns_to_ignore = to_save["config_columns_to_ignore"] - if "config_read_column" in to_save: - content.config_read_column = int(to_save["config_read_column"]) - if "config_theme" in to_save: - content.config_theme = int(to_save["config_theme"]) - if "config_title_regex" in to_save: - if content.config_title_regex != to_save["config_title_regex"]: - content.config_title_regex = to_save["config_title_regex"] - reboot_required = True - if "config_random_books" in to_save: - content.config_random_books = int(to_save["config_random_books"]) - if "config_books_per_page" in to_save: - content.config_books_per_page = int(to_save["config_books_per_page"]) - # Mature Content configuration - if "config_mature_content_tags" in to_save: - content.config_mature_content_tags = to_save["config_mature_content_tags"].strip() - - # Default user configuration - content.config_default_role = 0 - if "admin_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_ADMIN - if "download_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_DOWNLOAD - if "upload_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_UPLOAD - if "edit_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_EDIT - if "delete_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_DELETE_BOOKS - if "passwd_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_PASSWD - if "passwd_role" in to_save: - content.config_default_role = content.config_default_role + ub.ROLE_EDIT_SHELFS - content.config_default_show = 0 - if "show_detail_random" in to_save: - content.config_default_show = content.config_default_show + ub.DETAIL_RANDOM - if "show_language" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_LANGUAGE - if "show_series" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_SERIES - if "show_category" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_CATEGORY - if "show_hot" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_HOT - if "show_random" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_RANDOM - if "show_author" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_AUTHOR - if "show_publisher" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_PUBLISHER - if "show_best_rated" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_BEST_RATED - if "show_read_and_unread" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_READ_AND_UNREAD - if "show_recent" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_RECENT - if "show_sorted" in to_save: - content.config_default_show = content.config_default_show + ub.SIDEBAR_SORTED - if "show_mature_content" in to_save: - content.config_default_show = content.config_default_show + ub.MATURE_CONTENT - ub.session.commit() - flash(_(u"Calibre-Web configuration updated"), category="success") - config.loadSettings() - before_request() - if reboot_required: - # db.engine.dispose() # ToDo verify correct - # ub.session.close() - # ub.engine.dispose() - # stop Server - server.Server.setRestartTyp(True) - server.Server.stopServer() - app.logger.info('Reboot required, restarting') - readColumn = db.session.query(db.Custom_Columns)\ - .filter(db.and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() - return render_title_template("config_view_edit.html", content=config, readColumns=readColumn, - title=_(u"UI Configuration"), page="uiconfig") - - - -@web.route("/config", methods=["GET", "POST"]) -@unconfigured -def basic_configuration(): - logout_user() - return configuration_helper(1) - - -def configuration_helper(origin): - reboot_required = False - gdriveError=None - db_change = False - success = False - filedata = None - if gdriveutils.gdrive_support == False: - gdriveError = _('Import of optional Google Drive requirements missing') - else: - if not os.path.isfile(os.path.join(config.get_main_dir,'client_secrets.json')): - gdriveError = _('client_secrets.json is missing or not readable') - else: - with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings: - filedata=json.load(settings) - if not 'web' in filedata: - gdriveError = _('client_secrets.json is not configured for web application') - if request.method == "POST": - to_save = request.form.to_dict() - content = ub.session.query(ub.Settings).first() # type: ub.Settings - if "config_calibre_dir" in to_save: - if content.config_calibre_dir != to_save["config_calibre_dir"]: - content.config_calibre_dir = to_save["config_calibre_dir"] - db_change = True - # Google drive setup - if not os.path.isfile(os.path.join(config.get_main_dir, 'settings.yaml')): - content.config_use_google_drive = False - if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError: - if filedata: - if filedata['web']['redirect_uris'][0].endswith('/'): - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-1] - with open(os.path.join(config.get_main_dir,'settings.yaml'), 'w') as f: - yaml = "client_config_backend: settings\nclient_config_file: %(client_file)s\n" \ - "client_config:\n" \ - " client_id: %(client_id)s\n client_secret: %(client_secret)s\n" \ - " redirect_uri: %(redirect_uri)s\n\nsave_credentials: True\n" \ - "save_credentials_backend: file\nsave_credentials_file: %(credential)s\n\n" \ - "get_refresh_token: True\n\noauth_scope:\n" \ - " - https://www.googleapis.com/auth/drive\n" - f.write(yaml % {'client_file': os.path.join(config.get_main_dir,'client_secrets.json'), - 'client_id': filedata['web']['client_id'], - 'client_secret': filedata['web']['client_secret'], - 'redirect_uri': filedata['web']['redirect_uris'][0], - 'credential': os.path.join(config.get_main_dir,'gdrive_credentials')}) - else: - flash(_(u'client_secrets.json is not configured for web application'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") - # always show google drive settings, but in case of error deny support - if "config_use_google_drive" in to_save and not gdriveError: - content.config_use_google_drive = "config_use_google_drive" in to_save - else: - content.config_use_google_drive = 0 - if "config_google_drive_folder" in to_save: - if content.config_google_drive_folder != to_save["config_google_drive_folder"]: - content.config_google_drive_folder = to_save["config_google_drive_folder"] - gdriveutils.deleteDatabaseOnChange() - - if "config_port" in to_save: - if content.config_port != int(to_save["config_port"]): - content.config_port = int(to_save["config_port"]) - reboot_required = True - if "config_keyfile" in to_save: - if content.config_keyfile != to_save["config_keyfile"]: - if os.path.isfile(to_save["config_keyfile"]) or to_save["config_keyfile"] is u"": - content.config_keyfile = to_save["config_keyfile"] - reboot_required = True - else: - ub.session.commit() - flash(_(u'Keyfile location is not valid, please enter correct path'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") - if "config_certfile" in to_save: - if content.config_certfile != to_save["config_certfile"]: - if os.path.isfile(to_save["config_certfile"]) or to_save["config_certfile"] is u"": - content.config_certfile = to_save["config_certfile"] - reboot_required = True - else: - ub.session.commit() - flash(_(u'Certfile location is not valid, please enter correct path'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") - content.config_uploading = 0 - content.config_anonbrowse = 0 - content.config_public_reg = 0 - if "config_uploading" in to_save and to_save["config_uploading"] == "on": - content.config_uploading = 1 - if "config_anonbrowse" in to_save and to_save["config_anonbrowse"] == "on": - content.config_anonbrowse = 1 - if "config_public_reg" in to_save and to_save["config_public_reg"] == "on": - content.config_public_reg = 1 - - if "config_converterpath" in to_save: - content.config_converterpath = to_save["config_converterpath"].strip() - if "config_calibre" in to_save: - content.config_calibre = to_save["config_calibre"].strip() - if "config_ebookconverter" in to_save: - content.config_ebookconverter = int(to_save["config_ebookconverter"]) - - #LDAP configuratop, - if "config_use_ldap" in to_save and to_save["config_use_ldap"] == "on": - if not "config_ldap_provider_url" in to_save or not "config_ldap_dn" in to_save: - ub.session.commit() - flash(_(u'Please enter a LDAP provider and a DN'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") - else: - content.config_use_ldap = 1 - content.config_ldap_provider_url = to_save["config_ldap_provider_url"] - content.config_ldap_dn = to_save["config_ldap_dn"] - db_change = True - - # Remote login configuration - content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on") - if not content.config_remote_login: - ub.session.query(ub.RemoteAuthToken).delete() - - # Goodreads configuration - content.config_use_goodreads = ("config_use_goodreads" in to_save and to_save["config_use_goodreads"] == "on") - if "config_goodreads_api_key" in to_save: - content.config_goodreads_api_key = to_save["config_goodreads_api_key"] - if "config_goodreads_api_secret" in to_save: - content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] - if "config_updater" in to_save: - content.config_updatechannel = int(to_save["config_updater"]) - - # GitHub OAuth configuration - content.config_use_github_oauth = ("config_use_github_oauth" in to_save and to_save["config_use_github_oauth"] == "on") - if "config_github_oauth_client_id" in to_save: - content.config_github_oauth_client_id = to_save["config_github_oauth_client_id"] - if "config_github_oauth_client_secret" in to_save: - content.config_github_oauth_client_secret = to_save["config_github_oauth_client_secret"] - - if content.config_github_oauth_client_id != config.config_github_oauth_client_id or \ - content.config_github_oauth_client_secret != config.config_github_oauth_client_secret: - reboot_required = True - - # Google OAuth configuration - content.config_use_google_oauth = ("config_use_google_oauth" in to_save and to_save["config_use_google_oauth"] == "on") - if "config_google_oauth_client_id" in to_save: - content.config_google_oauth_client_id = to_save["config_google_oauth_client_id"] - if "config_google_oauth_client_secret" in to_save: - content.config_google_oauth_client_secret = to_save["config_google_oauth_client_secret"] - - if content.config_google_oauth_client_id != config.config_google_oauth_client_id or \ - content.config_google_oauth_client_secret != config.config_google_oauth_client_secret: - reboot_required = True - - if "config_log_level" in to_save: - content.config_log_level = int(to_save["config_log_level"]) - if content.config_logfile != to_save["config_logfile"]: - # check valid path, only path or file - if os.path.dirname(to_save["config_logfile"]): - if os.path.exists(os.path.dirname(to_save["config_logfile"])) and \ - os.path.basename(to_save["config_logfile"]) and not os.path.isdir(to_save["config_logfile"]): - content.config_logfile = to_save["config_logfile"] - else: - ub.session.commit() - flash(_(u'Logfile location is not valid, please enter correct path'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, title=_(u"Basic Configuration"), - page="config") - else: - content.config_logfile = to_save["config_logfile"] - reboot_required = True - - # Rarfile Content configuration - if "config_rarfile_location" in to_save and to_save['config_rarfile_location'] is not u"": - check = helper.check_unrar(to_save["config_rarfile_location"].strip()) - if not check[0] : - content.config_rarfile_location = to_save["config_rarfile_location"].strip() - else: - flash(check[1], category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, goodreads=goodreads_support, - rarfile_support=rar_support, title=_(u"Basic Configuration")) - try: - if content.config_use_google_drive and is_gdrive_ready() and not \ - os.path.exists(os.path.join(content.config_calibre_dir, "metadata.db")): - gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db") - if db_change: - if config.db_configured: - db.session.close() - db.engine.dispose() - ub.session.commit() - flash(_(u"Calibre-Web configuration updated"), category="success") - config.loadSettings() - app.logger.setLevel(config.config_log_level) - logging.getLogger("book_formats").setLevel(config.config_log_level) - except Exception as e: - flash(e, category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - goodreads=goodreads_support, rarfile_support=rar_support, - title=_(u"Basic Configuration"), page="config") - if db_change: - reload(db) - if not db.setup_db(): - flash(_(u'DB location is not valid, please enter correct path'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, - gdrive=gdriveutils.gdrive_support,gdriveError=gdriveError, - goodreads=goodreads_support, rarfile_support=rar_support, - title=_(u"Basic Configuration"), page="config") - if reboot_required: - # stop Server - server.Server.setRestartTyp(True) - server.Server.stopServer() - app.logger.info('Reboot required, restarting') - if origin: - success = True - if is_gdrive_ready() and gdriveutils.gdrive_support == True: # and config.config_use_google_drive == True: - gdrivefolders=gdriveutils.listRootFolders() - else: - gdrivefolders=list() - return render_title_template("config_edit.html", origin=origin, success=success, content=config, - show_authenticate_google_drive=not is_gdrive_ready(), - gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, - gdrivefolders=gdrivefolders, rarfile_support=rar_support, - goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") - - -@web.route("/admin/user/new", methods=["GET", "POST"]) -@login_required -@admin_required -def new_user(): - content = ub.User() - languages = speaking_language() - translations = [LC('en')] + babel.list_translations() - if request.method == "POST": - to_save = request.form.to_dict() - content.default_language = to_save["default_language"] - content.mature_content = "show_mature_content" in to_save - if "locale" in to_save: - content.locale = to_save["locale"] - content.sidebar_view = 0 - if "show_random" in to_save: - content.sidebar_view += ub.SIDEBAR_RANDOM - if "show_language" in to_save: - content.sidebar_view += ub.SIDEBAR_LANGUAGE - if "show_series" in to_save: - content.sidebar_view += ub.SIDEBAR_SERIES - if "show_category" in to_save: - content.sidebar_view += ub.SIDEBAR_CATEGORY - if "show_hot" in to_save: - content.sidebar_view += ub.SIDEBAR_HOT - if "show_read_and_unread" in to_save: - content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD - if "show_best_rated" in to_save: - content.sidebar_view += ub.SIDEBAR_BEST_RATED - if "show_author" in to_save: - content.sidebar_view += ub.SIDEBAR_AUTHOR - if "show_publisher" in to_save: - content.sidebar_view += ub.SIDEBAR_PUBLISHER - if "show_detail_random" in to_save: - content.sidebar_view += ub.DETAIL_RANDOM - if "show_sorted" in to_save: - content.sidebar_view += ub.SIDEBAR_SORTED - if "show_recent" in to_save: - content.sidebar_view += ub.SIDEBAR_RECENT - - content.role = 0 - if "admin_role" in to_save: - content.role = content.role + ub.ROLE_ADMIN - if "download_role" in to_save: - content.role = content.role + ub.ROLE_DOWNLOAD - if "upload_role" in to_save: - content.role = content.role + ub.ROLE_UPLOAD - if "edit_role" in to_save: - content.role = content.role + ub.ROLE_EDIT - if "delete_role" in to_save: - content.role = content.role + ub.ROLE_DELETE_BOOKS - if "passwd_role" in to_save: - content.role = content.role + ub.ROLE_PASSWD - if "edit_shelf_role" in to_save: - content.role = content.role + ub.ROLE_EDIT_SHELFS - if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: - flash(_(u"Please fill out all fields!"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - title=_(u"Add new user")) - content.password = generate_password_hash(to_save["password"]) - content.nickname = to_save["nickname"] - if config.config_public_reg and not check_valid_domain(to_save["email"]): - flash(_(u"E-mail is not from valid domain"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - title=_(u"Add new user")) - else: - content.email = to_save["email"] - try: - ub.session.add(content) - ub.session.commit() - flash(_(u"User '%(user)s' created", user=content.nickname), category="success") - return redirect(url_for('admin')) - except IntegrityError: - ub.session.rollback() - flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") - else: - content.role = config.config_default_role - content.sidebar_view = config.config_default_show - content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT) - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - languages=languages, title=_(u"Add new user"), page="newuser") - - -@web.route("/admin/mailsettings", methods=["GET", "POST"]) -@login_required -@admin_required -def edit_mailsettings(): - content = ub.session.query(ub.Settings).first() - if request.method == "POST": - to_save = request.form.to_dict() - content.mail_server = to_save["mail_server"] - content.mail_port = int(to_save["mail_port"]) - content.mail_login = to_save["mail_login"] - content.mail_password = to_save["mail_password"] - content.mail_from = to_save["mail_from"] - content.mail_use_ssl = int(to_save["mail_use_ssl"]) - try: - ub.session.commit() - except Exception as e: - flash(e, category="error") - if "test" in to_save and to_save["test"]: - if current_user.kindle_mail: - result = helper.send_test_mail(current_user.kindle_mail, current_user.nickname) - if result is None: - flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), - category="success") - else: - flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") - else: - flash(_(u"Please configure your kindle e-mail address first..."), category="error") - else: - flash(_(u"E-mail server settings updated"), category="success") - return render_title_template("email_edit.html", content=content, title=_(u"Edit e-mail server settings"), - page="mailset") - - -@web.route("/admin/user/", methods=["GET", "POST"]) -@login_required -@admin_required -def edit_user(user_id): - content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User - downloads = list() - languages = speaking_language() - translations = babel.list_translations() + [LC('en')] - for book in content.downloads: - downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() - if downloadbook: - downloads.append(downloadbook) - else: - ub.delete_download(book.book_id) - # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete() - # ub.session.commit() - if request.method == "POST": - to_save = request.form.to_dict() - if "delete" in to_save: - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session.commit() - flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") - return redirect(url_for('admin')) - else: - if "password" in to_save and to_save["password"]: - content.password = generate_password_hash(to_save["password"]) - - if "admin_role" in to_save and not content.role_admin(): - content.role = content.role + ub.ROLE_ADMIN - elif "admin_role" not in to_save and content.role_admin(): - content.role = content.role - ub.ROLE_ADMIN - - if "download_role" in to_save and not content.role_download(): - content.role = content.role + ub.ROLE_DOWNLOAD - elif "download_role" not in to_save and content.role_download(): - content.role = content.role - ub.ROLE_DOWNLOAD - - if "upload_role" in to_save and not content.role_upload(): - content.role = content.role + ub.ROLE_UPLOAD - elif "upload_role" not in to_save and content.role_upload(): - content.role = content.role - ub.ROLE_UPLOAD - - if "edit_role" in to_save and not content.role_edit(): - content.role = content.role + ub.ROLE_EDIT - elif "edit_role" not in to_save and content.role_edit(): - content.role = content.role - ub.ROLE_EDIT - - if "delete_role" in to_save and not content.role_delete_books(): - content.role = content.role + ub.ROLE_DELETE_BOOKS - elif "delete_role" not in to_save and content.role_delete_books(): - content.role = content.role - ub.ROLE_DELETE_BOOKS - - if "passwd_role" in to_save and not content.role_passwd(): - content.role = content.role + ub.ROLE_PASSWD - elif "passwd_role" not in to_save and content.role_passwd(): - content.role = content.role - ub.ROLE_PASSWD - - if "edit_shelf_role" in to_save and not content.role_edit_shelfs(): - content.role = content.role + ub.ROLE_EDIT_SHELFS - elif "edit_shelf_role" not in to_save and content.role_edit_shelfs(): - content.role = content.role - ub.ROLE_EDIT_SHELFS - - if "show_random" in to_save and not content.show_random_books(): - content.sidebar_view += ub.SIDEBAR_RANDOM - elif "show_random" not in to_save and content.show_random_books(): - content.sidebar_view -= ub.SIDEBAR_RANDOM - - if "show_language" in to_save and not content.show_language(): - content.sidebar_view += ub.SIDEBAR_LANGUAGE - elif "show_language" not in to_save and content.show_language(): - content.sidebar_view -= ub.SIDEBAR_LANGUAGE - - if "show_series" in to_save and not content.show_series(): - content.sidebar_view += ub.SIDEBAR_SERIES - elif "show_series" not in to_save and content.show_series(): - content.sidebar_view -= ub.SIDEBAR_SERIES - - if "show_category" in to_save and not content.show_category(): - content.sidebar_view += ub.SIDEBAR_CATEGORY - elif "show_category" not in to_save and content.show_category(): - content.sidebar_view -= ub.SIDEBAR_CATEGORY - - if "show_recent" in to_save and not content.show_recent(): - content.sidebar_view += ub.SIDEBAR_RECENT - elif "show_recent" not in to_save and content.show_recent(): - content.sidebar_view -= ub.SIDEBAR_RECENT - - if "show_sorted" in to_save and not content.show_sorted(): - content.sidebar_view += ub.SIDEBAR_SORTED - elif "show_sorted" not in to_save and content.show_sorted(): - content.sidebar_view -= ub.SIDEBAR_SORTED - - if "show_publisher" in to_save and not content.show_publisher(): - content.sidebar_view += ub.SIDEBAR_PUBLISHER - elif "show_publisher" not in to_save and content.show_publisher(): - content.sidebar_view -= ub.SIDEBAR_PUBLISHER - - if "show_hot" in to_save and not content.show_hot_books(): - content.sidebar_view += ub.SIDEBAR_HOT - elif "show_hot" not in to_save and content.show_hot_books(): - content.sidebar_view -= ub.SIDEBAR_HOT - - if "show_best_rated" in to_save and not content.show_best_rated_books(): - content.sidebar_view += ub.SIDEBAR_BEST_RATED - elif "show_best_rated" not in to_save and content.show_best_rated_books(): - content.sidebar_view -= ub.SIDEBAR_BEST_RATED - - if "show_read_and_unread" in to_save and not content.show_read_and_unread(): - content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD - elif "show_read_and_unread" not in to_save and content.show_read_and_unread(): - content.sidebar_view -= ub.SIDEBAR_READ_AND_UNREAD - - if "show_author" in to_save and not content.show_author(): - content.sidebar_view += ub.SIDEBAR_AUTHOR - elif "show_author" not in to_save and content.show_author(): - content.sidebar_view -= ub.SIDEBAR_AUTHOR - - if "show_detail_random" in to_save and not content.show_detail_random(): - content.sidebar_view += ub.DETAIL_RANDOM - elif "show_detail_random" not in to_save and content.show_detail_random(): - content.sidebar_view -= ub.DETAIL_RANDOM - - content.mature_content = "show_mature_content" in to_save - - if "default_language" in to_save: - content.default_language = to_save["default_language"] - if "locale" in to_save and to_save["locale"]: - content.locale = to_save["locale"] - if to_save["email"] and to_save["email"] != content.email: - content.email = to_save["email"] - if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: - content.kindle_mail = to_save["kindle_mail"] - try: - ub.session.commit() - flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") - except IntegrityError: - ub.session.rollback() - flash(_(u"An unknown error occured."), category="error") - return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, - content=content, downloads=downloads, title=_(u"Edit User %(nick)s", - nick=content.nickname), page="edituser") - - -@web.route("/admin/resetpassword/") -@login_required -@admin_required -def reset_password(user_id): - if not config.config_public_reg: - abort(404) - if current_user is not None and current_user.is_authenticated: - existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first() - password = helper.generate_random_password() - existing_user.password = generate_password_hash(password) - try: - ub.session.commit() - helper.send_registration_mail(existing_user.email, existing_user.nickname, password, True) - flash(_(u"Password for user %(user)s reset", user=existing_user.nickname), category="success") - except Exception: - ub.session.rollback() - flash(_(u"An unknown error occurred. Please try again later."), category="error") - return redirect(url_for('admin')) - - -def render_edit_book(book_id): - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - book = db.session.query(db.Books)\ - .filter(db.Books.id == book_id).filter(common_filters()).first() - - if not book: - flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") - return redirect(url_for("web.index")) - - for indx in range(0, len(book.languages)): - book.languages[indx].language_name = language_table[get_locale()][book.languages[indx].lang_code] - - book = order_authors(book) - - author_names = [] - for authr in book.authors: - author_names.append(authr.name.replace('|', ',')) - - # Option for showing convertbook button - valid_source_formats=list() - if config.config_ebookconverter == 2: - for file in book.data: - if file.format.lower() in EXTENSIONS_CONVERT: - valid_source_formats.append(file.format.lower()) - - # Determine what formats don't already exist - allowed_conversion_formats = EXTENSIONS_CONVERT.copy() - for file in book.data: - try: - allowed_conversion_formats.remove(file.format.lower()) - except Exception: - app.logger.warning(file.format.lower() + ' already removed from list.') - - return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, - title=_(u"edit metadata"), page="editbook", - conversion_formats=allowed_conversion_formats, - source_formats=valid_source_formats) - - -def edit_cc_data(book_id, book, to_save): - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - for c in cc: - cc_string = "custom_column_" + str(c.id) - if not c.is_multiple: - if len(getattr(book, cc_string)) > 0: - cc_db_value = getattr(book, cc_string)[0].value - else: - cc_db_value = None - if to_save[cc_string].strip(): - if c.datatype == 'bool': - if to_save[cc_string] == 'None': - to_save[cc_string] = None - else: - to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0 - if to_save[cc_string] != cc_db_value: - if cc_db_value is not None: - if to_save[cc_string] is not None: - setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) - else: - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - db.session.delete(del_cc) - else: - cc_class = db.cc_classes[c.id] - new_cc = cc_class(value=to_save[cc_string], book=book_id) - db.session.add(new_cc) - elif c.datatype == 'int': - if to_save[cc_string] == 'None': - to_save[cc_string] = None - if to_save[cc_string] != cc_db_value: - if cc_db_value is not None: - if to_save[cc_string] is not None: - setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) - else: - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - db.session.delete(del_cc) - else: - cc_class = db.cc_classes[c.id] - new_cc = cc_class(value=to_save[cc_string], book=book_id) - db.session.add(new_cc) - - else: - if c.datatype == 'rating': - to_save[cc_string] = str(int(float(to_save[cc_string]) * 2)) - if to_save[cc_string].strip() != cc_db_value: - if cc_db_value is not None: - # remove old cc_val - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if len(del_cc.books) == 0: - db.session.delete(del_cc) - cc_class = db.cc_classes[c.id] - new_cc = db.session.query(cc_class).filter( - cc_class.value == to_save[cc_string].strip()).first() - # if no cc val is found add it - if new_cc is None: - new_cc = cc_class(value=to_save[cc_string].strip()) - db.session.add(new_cc) - db.session.flush() - new_cc = db.session.query(cc_class).filter( - cc_class.value == to_save[cc_string].strip()).first() - # add cc value to book - getattr(book, cc_string).append(new_cc) - else: - if cc_db_value is not None: - # remove old cc_val - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if len(del_cc.books) == 0: - db.session.delete(del_cc) - else: - input_tags = to_save[cc_string].split(',') - input_tags = list(map(lambda it: it.strip(), input_tags)) - modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session, - 'custom') - return cc - -def upload_single_file(request, book, book_id): - # Check and handle Uploaded file - if 'btn-upload-format' in request.files: - requested_file = request.files['btn-upload-format'] - # check for empty request - if requested_file.filename != '': - if '.' in requested_file.filename: - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in EXTENSIONS_UPLOAD: - flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), - category="error") - return redirect(url_for('show_book', book_id=book.id)) - else: - flash(_('File to be uploaded must have an extension'), category="error") - return redirect(url_for('show_book', book_id=book.id)) - - file_name = book.path.rsplit('/', 1)[-1] - filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) - saved_filename = os.path.join(filepath, file_name + '.' + file_ext) - - # check if file path exists, otherwise create it, copy file to calibre path and delete temp file - if not os.path.exists(filepath): - try: - os.makedirs(filepath) - except OSError: - flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") - return redirect(url_for('show_book', book_id=book.id)) - try: - requested_file.save(saved_filename) - except OSError: - flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") - return redirect(url_for('show_book', book_id=book.id)) - - file_size = os.path.getsize(saved_filename) - is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\ - filter(db.Data.format == file_ext.upper()).first() - - # Format entry already exists, no need to update the database - if is_format: - app.logger.info('Book format already existing') - else: - db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) - db.session.add(db_format) - db.session.commit() - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - - # Queue uploader info - uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) - helper.global_WorkerThread.add_upload(current_user.nickname, - "" + uploadText + "") - -def upload_cover(request, book): - if 'btn-upload-cover' in request.files: - requested_file = request.files['btn-upload-cover'] - # check for empty request - if requested_file.filename != '': - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) - saved_filename = os.path.join(filepath, 'cover.' + file_ext) - - # check if file path exists, otherwise create it, copy file to calibre path and delete temp file - if not os.path.exists(filepath): - try: - os.makedirs(filepath) - except OSError: - flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), - category="error") - return redirect(url_for('show_book', book_id=book.id)) - try: - requested_file.save(saved_filename) - # im=Image.open(saved_filename) - book.has_cover = 1 - except OSError: - flash(_(u"Failed to store cover-file %(cover)s.", cover=saved_filename), category="error") - return redirect(url_for('show_book', book_id=book.id)) - except IOError: - flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error") - return redirect(url_for('show_book', book_id=book.id)) - -@web.route("/admin/book/", methods=['GET', 'POST']) -@login_required_if_no_ano -@edit_required -def edit_book(book_id): - # Show form - if request.method != 'POST': - return render_edit_book(book_id) - - # create the function for sorting... - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - book = db.session.query(db.Books)\ - .filter(db.Books.id == book_id).filter(common_filters()).first() - - # Book not found - if not book: - flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") - return redirect(url_for("web.index")) - - upload_single_file(request, book, book_id) - upload_cover(request, book) - try: - to_save = request.form.to_dict() - # Update book - edited_books_id = None - #handle book title - if book.title != to_save["book_title"].rstrip().strip(): - if to_save["book_title"] == '': - to_save["book_title"] = _(u'unknown') - book.title = to_save["book_title"].rstrip().strip() - edited_books_id = book.id - - # handle author(s) - input_authors = to_save["author_name"].split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - # we have all author names now - if input_authors == ['']: - input_authors = [_(u'unknown')] # prevent empty Author - - modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author') - - # Search for each author if author is in database, if not, authorname and sorted authorname is generated new - # everything then is assembled for sorted author field in database - sort_authors_list = list() - for inp in input_authors: - stored_author = db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - stored_author = helper.get_sorted_author(inp) - else: - stored_author = stored_author.sort - sort_authors_list.append(helper.get_sorted_author(stored_author)) - sort_authors = ' & '.join(sort_authors_list) - if book.author_sort != sort_authors: - edited_books_id = book.id - book.author_sort = sort_authors - - - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - - error = False - if edited_books_id: - error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0]) - - if not error: - if to_save["cover_url"]: - if helper.save_cover(to_save["cover_url"], book.path) is True: - book.has_cover = 1 - else: - flash(_(u"Cover is not a jpg file, can't save"), category="error") - - if book.series_index != to_save["series_index"]: - book.series_index = to_save["series_index"] - - # Handle book comments/description - if len(book.comments): - book.comments[0].text = to_save["description"] - else: - book.comments.append(db.Comments(text=to_save["description"], book=book.id)) - - # Handle book tags - input_tags = to_save["tags"].split(',') - input_tags = list(map(lambda it: it.strip(), input_tags)) - modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags') - - # Handle book series - input_series = [to_save["series"].strip()] - input_series = [x for x in input_series if x != ''] - modify_database_object(input_series, book.series, db.Series, db.session, 'series') - - if to_save["pubdate"]: - try: - book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d") - except ValueError: - book.pubdate = db.Books.DEFAULT_PUBDATE - else: - book.pubdate = db.Books.DEFAULT_PUBDATE - - if to_save["publisher"]: - publisher = to_save["publisher"].rstrip().strip() - if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): - modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher') - elif len(book.publishers): - modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher') - - - # handle book languages - input_languages = to_save["languages"].split(',') - input_languages = [x.strip().lower() for x in input_languages if x != ''] - input_l = [] - invers_lang_table = [x.lower() for x in language_table[get_locale()].values()] - for lang in input_languages: - try: - res = list(language_table[get_locale()].keys())[invers_lang_table.index(lang)] - input_l.append(res) - except ValueError: - app.logger.error('%s is not a valid language' % lang) - flash(_(u"%(langname)s is not a valid language", langname=lang), category="error") - modify_database_object(input_l, book.languages, db.Languages, db.session, 'languages') - - # handle book ratings - if to_save["rating"].strip(): - old_rating = False - if len(book.ratings) > 0: - old_rating = book.ratings[0].rating - ratingx2 = int(float(to_save["rating"]) * 2) - if ratingx2 != old_rating: - is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first() - if is_rating: - book.ratings.append(is_rating) - else: - new_rating = db.Ratings(rating=ratingx2) - book.ratings.append(new_rating) - if old_rating: - book.ratings.remove(book.ratings[0]) - else: - if len(book.ratings) > 0: - book.ratings.remove(book.ratings[0]) - - # handle cc data - edit_cc_data(book_id, book, to_save) - - db.session.commit() - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - if "detail_view" in to_save: - return redirect(url_for('show_book', book_id=book.id)) - else: - flash(_("Metadata successfully updated"), category="success") - return render_edit_book(book_id) - else: - db.session.rollback() - flash(error, category="error") - return render_edit_book(book_id) - except Exception as e: - app.logger.exception(e) - db.session.rollback() - flash(_("Error editing book, please check logfile for details"), category="error") - return redirect(url_for('show_book', book_id=book.id)) - - -@web.route("/upload", methods=["GET", "POST"]) -@login_required_if_no_ano -@upload_required -def upload(): - if not config.config_uploading: - abort(404) - if request.method == 'POST' and 'btn-upload' in request.files: - for requested_file in request.files.getlist("btn-upload"): - # create the function for sorting... - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) - - # check if file extension is correct - if '.' in requested_file.filename: - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in EXTENSIONS_UPLOAD: - flash( - _("File extension '%(ext)s' is not allowed to be uploaded to this server", - ext=file_ext), category="error") - return redirect(url_for('index')) - else: - flash(_('File to be uploaded must have an extension'), category="error") - return redirect(url_for('index')) - - # extract metadata from file - meta = uploader.upload(requested_file) - title = meta.title - authr = meta.author - tags = meta.tags - series = meta.series - series_index = meta.series_id - title_dir = helper.get_valid_filename(title) - author_dir = helper.get_valid_filename(authr) - filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir) - saved_filename = os.path.join(filepath, title_dir + meta.extension.lower()) - - # check if file path exists, otherwise create it, copy file to calibre path and delete temp file - if not os.path.exists(filepath): - try: - os.makedirs(filepath) - except OSError: - flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") - return redirect(url_for('index')) - try: - copyfile(meta.file_path, saved_filename) - except OSError: - flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") - return redirect(url_for('index')) - try: - os.unlink(meta.file_path) - except OSError: - flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path), - category="warning") - - if meta.cover is None: - has_cover = 0 - copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"), - os.path.join(filepath, "cover.jpg")) - else: - has_cover = 1 - move(meta.cover, os.path.join(filepath, "cover.jpg")) - - # handle authors - is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first() - if is_author: - db_author = is_author - else: - db_author = db.Authors(authr, helper.get_sorted_author(authr), "") - db.session.add(db_author) - - # handle series - db_series = None - is_series = db.session.query(db.Series).filter(db.Series.name == series).first() - if is_series: - db_series = is_series - elif series != '': - db_series = db.Series(series, "") - db.session.add(db_series) - - # add language actually one value in list - input_language = meta.languages - db_language = None - if input_language != "": - input_language = isoLanguages.get(name=input_language).part3 - hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first() - if hasLanguage: - db_language = hasLanguage - else: - db_language = db.Languages(input_language) - db.session.add(db_language) - - # combine path and normalize path from windows systems - path = os.path.join(author_dir, title_dir).replace('\\', '/') - db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), - series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language) - db_book.authors.append(db_author) - if db_series: - db_book.series.append(db_series) - if db_language is not None: - db_book.languages.append(db_language) - file_size = os.path.getsize(saved_filename) - db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) - - # handle tags - input_tags = tags.split(',') - input_tags = list(map(lambda it: it.strip(), input_tags)) - if input_tags[0] !="": - modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags') - - # flush content, get db_book.id available - db_book.data.append(db_data) - db.session.add(db_book) - db.session.flush() - - # add comment - book_id = db_book.id - upload_comment = Markup(meta.description).unescape() - if upload_comment != "": - db.session.add(db.Comments(upload_comment, book_id)) - - # save data to database, reread data - db.session.commit() - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() - - # upload book to gdrive if nesseccary and add "(bookid)" to folder name - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - error = helper.update_dir_stucture(book.id, config.config_calibre_dir) - db.session.commit() - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - if error: - flash(error, category="error") - uploadText=_(u"File %(file)s uploaded", file=book.title) - helper.global_WorkerThread.add_upload(current_user.nickname, - "" + uploadText + "") - - # create data for displaying display Full language name instead of iso639.part3language - if db_language is not None: - book.languages[0].language_name = _(meta.languages) - author_names = [] - for author in db_book.authors: - author_names.append(author.name) - if len(request.files.getlist("btn-upload")) < 2: - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns. - datatype.notin_(db.cc_exceptions)).all() - if current_user.role_edit() or current_user.role_admin(): - return render_title_template('book_edit.html', book=book, authors=author_names, - cc=cc, title=_(u"edit metadata"), page="upload") - book_in_shelfs = [] - kindle_list = helper.check_send_to_kindle(book) - reader_list = helper.check_read_formats(book) - - return render_title_template('detail.html', entry=book, cc=cc, - title=book.title, books_shelfs=book_in_shelfs, kindle_list=kindle_list, - reader_list=reader_list, page="upload") - return redirect(url_for("web.index")) - - -@web.route("/admin/book/convert/", methods=['POST']) -@login_required_if_no_ano -@edit_required -def convert_bookformat(book_id): - # check to see if we have form fields to work with - if not send user back - book_format_from = request.form.get('book_format_from', None) - book_format_to = request.form.get('book_format_to', None) - - if (book_format_from is None) or (book_format_to is None): - flash(_(u"Source or destination format for conversion missing"), category="error") - return redirect(request.environ["HTTP_REFERER"]) - - app.logger.debug('converting: book id: ' + str(book_id) + - ' from: ' + request.form['book_format_from'] + - ' to: ' + request.form['book_format_to']) - rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), - book_format_to.upper(), current_user.nickname) - - if rtn is None: - flash(_(u"Book successfully queued for converting to %(book_format)s", - book_format=book_format_to), - category="success") - else: - flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") - return redirect(request.environ["HTTP_REFERER"]) - -''' -def register_oauth_blueprint(blueprint, show_name): - if blueprint.name != "": - oauth_check[blueprint.name] = show_name - - -def register_user_with_oauth(user=None): - all_oauth = {} - for oauth in oauth_check.keys(): - if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '': - all_oauth[oauth] = oauth_check[oauth] - if len(all_oauth.keys()) == 0: - return - if user is None: - flash(_(u"Register with %s" % ", ".join(list(all_oauth.values()))), category="success") - else: - for oauth in all_oauth.keys(): - # Find this OAuth token in the database, or create it - query = ub.session.query(ub.OAuth).filter_by( - provider=oauth, - provider_user_id=session[oauth + "_oauth_user_id"], - ) - try: - oauth = query.one() - oauth.user_id = user.id - except NoResultFound: - # no found, return error - return - try: - ub.session.commit() - except Exception as e: - app.logger.exception(e) - ub.session.rollback() - - -def logout_oauth_user(): - for oauth in oauth_check.keys(): - if oauth + '_oauth_user_id' in session: - session.pop(oauth + '_oauth_user_id') - - -github_blueprint = make_github_blueprint( - client_id=config.config_github_oauth_client_id, - client_secret=config.config_github_oauth_client_secret, - redirect_to="github_login",) - -google_blueprint = make_google_blueprint( - client_id=config.config_google_oauth_client_id, - client_secret=config.config_google_oauth_client_secret, - redirect_to="google_login", - scope=[ - "https://www.googleapis.com/auth/plus.me", - "https://www.googleapis.com/auth/userinfo.email", - ] -) - -app.register_blueprint(google_blueprint, url_prefix="/login") -app.register_blueprint(github_blueprint, url_prefix='/login') - -github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) -google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) - - -if config.config_use_github_oauth: - register_oauth_blueprint(github_blueprint, 'GitHub') -if config.config_use_google_oauth: - register_oauth_blueprint(google_blueprint, 'Google') - - -@oauth_authorized.connect_via(github_blueprint) -def github_logged_in(blueprint, token): - if not token: - flash(_("Failed to log in with GitHub."), category="error") - return False - - resp = blueprint.session.get("/user") - if not resp.ok: - flash(_("Failed to fetch user info from GitHub."), category="error") - return False - - github_info = resp.json() - github_user_id = str(github_info["id"]) - return oauth_update_token(blueprint, token, github_user_id) - - -@oauth_authorized.connect_via(google_blueprint) -def google_logged_in(blueprint, token): - if not token: - flash(_("Failed to log in with Google."), category="error") - return False - - resp = blueprint.session.get("/oauth2/v2/userinfo") - if not resp.ok: - flash(_("Failed to fetch user info from Google."), category="error") - return False - - google_info = resp.json() - google_user_id = str(google_info["id"]) - - return oauth_update_token(blueprint, token, google_user_id) - - -def oauth_update_token(blueprint, token, provider_user_id): - session[blueprint.name + "_oauth_user_id"] = provider_user_id - session[blueprint.name + "_oauth_token"] = token - - # Find this OAuth token in the database, or create it - query = ub.session.query(ub.OAuth).filter_by( - provider=blueprint.name, - provider_user_id=provider_user_id, - ) - try: - oauth = query.one() - # update token - oauth.token = token - except NoResultFound: - oauth = ub.OAuth( - provider=blueprint.name, - provider_user_id=provider_user_id, - token=token, - ) - try: - ub.session.add(oauth) - ub.session.commit() - except Exception as e: - app.logger.exception(e) - ub.session.rollback() - - # Disable Flask-Dance's default behavior for saving the OAuth token - return False - - -def bind_oauth_or_register(provider, provider_user_id, redirect_url): - query = ub.session.query(ub.OAuth).filter_by( - provider=provider, - provider_user_id=provider_user_id, - ) - try: - oauth = query.one() - # already bind with user, just login - if oauth.user: - login_user(oauth.user) - return redirect(url_for('index')) - else: - # bind to current user - if current_user and current_user.is_authenticated: - oauth.user = current_user - try: - ub.session.add(oauth) - ub.session.commit() - except Exception as e: - app.logger.exception(e) - ub.session.rollback() - return redirect(url_for('register')) - except NoResultFound: - return redirect(url_for(redirect_url)) - - -def get_oauth_status(): - status = [] - query = ub.session.query(ub.OAuth).filter_by( - user_id=current_user.id, - ) - try: - oauths = query.all() - for oauth in oauths: - status.append(oauth.provider) - return status - except NoResultFound: - return None - - -def unlink_oauth(provider): - if request.host_url + 'me' != request.referrer: - pass - query = ub.session.query(ub.OAuth).filter_by( - provider=provider, - user_id=current_user.id, - ) - try: - oauth = query.one() - if current_user and current_user.is_authenticated: - oauth.user = current_user - try: - ub.session.delete(oauth) - ub.session.commit() - logout_oauth_user() - flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") - except Exception as e: - app.logger.exception(e) - ub.session.rollback() - flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") - except NoResultFound: - app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) - flash(_("Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") - return redirect(url_for('profile')) - - -# notify on OAuth provider error -@oauth_error.connect_via(github_blueprint) -def github_error(blueprint, error, error_description=None, error_uri=None): - msg = ( - "OAuth error from {name}! " - "error={error} description={description} uri={uri}" - ).format( - name=blueprint.name, - error=error, - description=error_description, - uri=error_uri, - ) - flash(msg, category="error") - - -@web.route('/github') -@github_oauth_required -def github_login(): - if not github.authorized: - return redirect(url_for('github.login')) - account_info = github.get('/user') - if account_info.ok: - account_info_json = account_info.json() - return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') - flash(_(u"GitHub Oauth error, please retry later."), category="error") - return redirect(url_for('login')) - - -@web.route('/unlink/github', methods=["GET"]) -@login_required -def github_login_unlink(): - return unlink_oauth(github_blueprint.name) - - -@web.route('/google') -@google_oauth_required -def google_login(): - if not google.authorized: - return redirect(url_for("google.login")) - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - account_info_json = resp.json() - return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') - flash(_(u"Google Oauth error, please retry later."), category="error") - return redirect(url_for('login')) - - -@oauth_error.connect_via(google_blueprint) -def google_error(blueprint, error, error_description=None, error_uri=None): - msg = ( - "OAuth error from {name}! " - "error={error} description={description} uri={uri}" - ).format( - name=blueprint.name, - error=error, - description=error_description, - uri=error_uri, - ) - flash(msg, category="error") - - -@web.route('/unlink/google', methods=["GET"]) -@login_required -def google_login_unlink(): - return unlink_oauth(google_blueprint.name) -'''