This commit is contained in:
cbartondock 2021-10-15 15:58:11 -04:00
commit 5aefc893de
131 changed files with 3281 additions and 2113 deletions

View File

@ -62,7 +62,7 @@ Please note that running the above install command can fail on some versions of
## Requirements ## Requirements
python 3.x+ python 3.5+
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata: Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:

13
cps.py
View File

@ -17,18 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function, unicode_literals
import sys import sys
import os import os
# Insert local directories into path # Insert local directories into path
if sys.version_info < (3, 0): sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8')))) sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor'))
else:
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
from cps import create_app from cps import create_app
@ -42,13 +37,14 @@ from cps.admin import admi
from cps.gdrive import gdrive from cps.gdrive import gdrive
from cps.editbooks import editbook from cps.editbooks import editbook
from cps.remotelogin import remotelogin from cps.remotelogin import remotelogin
from cps.search_metadata import meta
from cps.error_handler import init_errorhandler from cps.error_handler import init_errorhandler
try: try:
from cps.kobo import kobo, get_kobo_activated from cps.kobo import kobo, get_kobo_activated
from cps.kobo_auth import kobo_auth from cps.kobo_auth import kobo_auth
kobo_available = get_kobo_activated() kobo_available = get_kobo_activated()
except ImportError: except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False kobo_available = False
try: try:
@ -70,6 +66,7 @@ def main():
app.register_blueprint(shelf) app.register_blueprint(shelf)
app.register_blueprint(admi) app.register_blueprint(admi)
app.register_blueprint(remotelogin) app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive) app.register_blueprint(gdrive)
app.register_blueprint(editbook) app.register_blueprint(editbook)
if kobo_available: if kobo_available:

34
cps/MyLoginManager.py Normal file
View File

@ -0,0 +1,34 @@
# -*- 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, GammaC0de, vuolter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_login import LoginManager
from flask import session
class MyLoginManager(LoginManager):
def _session_protection_failed(self):
sess = session._get_current_object()
ident = self._session_identifier_generator()
if(sess and not (len(sess) == 1 and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
return super(). _session_protection_failed()
return False

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import mimetypes import mimetypes
@ -29,7 +28,7 @@ from babel import Locale as LC
from babel import negotiate_locale from babel import negotiate_locale
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask import Flask, request, g from flask import Flask, request, g
from flask_login import LoginManager from .MyLoginManager import MyLoginManager
from flask_babel import Babel from flask_babel import Babel
from flask_principal import Principal from flask_principal import Principal
@ -43,6 +42,12 @@ try:
except ImportError: except ImportError:
lxml_present = False lxml_present = False
try:
from flask_wtf.csrf import CSRFProtect
wtf_present = True
except ImportError:
wtf_present = False
mimetypes.init() mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub') mimetypes.add_type('application/epub+zip', '.epub')
@ -70,11 +75,17 @@ app.config.update(
) )
lm = LoginManager() lm = MyLoginManager()
lm.login_view = 'web.login' lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong' lm.session_protection = 'strong'
if wtf_present:
csrf = CSRFProtect()
csrf.init_app(app)
else:
csrf = None
ub.init_db(cli.settingspath) ub.init_db(cli.settingspath)
# pylint: disable=no-member # pylint: disable=no-member
config = config_sql.load_configuration(ub.session) config = config_sql.load_configuration(ub.session)
@ -105,12 +116,12 @@ def create_app():
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
sys.exit(6) sys.exit(6)
if not wtf_present:
log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
# sys.exit(7)
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
# For python2 convert path to unicode
if sys.version_info < (3, 0):
app.static_folder = app.static_folder.decode('utf-8')
app.root_path = app.root_path.decode('utf-8')
app.instance_path = app.instance_path.decode('utf-8')
if os.environ.get('FLASK_DEBUG'): if os.environ.get('FLASK_DEBUG'):
cache_buster.init_cache_busting(app) cache_buster.init_cache_busting(app)
@ -139,7 +150,6 @@ def create_app():
def get_locale(): def get_locale():
# if a user is logged in, use the locale from the user settings # if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None) user = getattr(g, 'user', None)
# user = None
if user is not None and hasattr(user, "locale"): if user is not None and hasattr(user, "locale"):
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale return user.locale
@ -160,6 +170,7 @@ def get_timezone():
user = getattr(g, 'user', None) user = getattr(g, 'user', None)
return user.timezone if user else None return user.timezone if user else None
from .updater import Updater from .updater import Updater
updater_thread = Updater() updater_thread = Updater()
updater_thread.start() updater_thread.start()

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import platform import platform
import sqlite3 import sqlite3
@ -29,6 +28,10 @@ from collections import OrderedDict
import babel, pytz, requests, sqlalchemy import babel, pytz, requests, sqlalchemy
import werkzeug, flask, flask_login, flask_principal, jinja2 import werkzeug, flask, flask_login, flask_principal, jinja2
from flask_babel import gettext as _ from flask_babel import gettext as _
try:
from flask_wtf import __version__ as flaskwtf_version
except ImportError:
flaskwtf_version = _(u'not installed')
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
from .render_template import render_title_template from .render_template import render_title_template
@ -75,6 +78,7 @@ _VERSIONS = OrderedDict(
Flask=flask.__version__, Flask=flask.__version__,
Flask_Login=flask_loginVersion, Flask_Login=flask_loginVersion,
Flask_Principal=flask_principal.__version__, Flask_Principal=flask_principal.__version__,
Flask_WTF=flaskwtf_version,
Werkzeug=werkzeug.__version__, Werkzeug=werkzeug.__version__,
Babel=babel.__version__, Babel=babel.__version__,
Jinja2=jinja2.__version__, Jinja2=jinja2.__version__,
@ -84,14 +88,14 @@ _VERSIONS = OrderedDict(
SQLite=sqlite3.sqlite_version, SQLite=sqlite3.sqlite_version,
iso639=isoLanguages.__version__, iso639=isoLanguages.__version__,
pytz=pytz.__version__, pytz=pytz.__version__,
Unidecode = unidecode_version, Unidecode=unidecode_version,
Scholarly = scholarly_version, Scholarly=scholarly_version,
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None, Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
Goodreads = u'installed' if bool(services.goodreads_support) else None, Goodreads=u'installed' if bool(services.goodreads_support) else None,
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None, jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
flask_dance = flask_danceVersion, flask_dance=flask_danceVersion,
greenlet = greenlet_Version greenlet=greenlet_Version
) )
_VERSIONS.update(uploader.get_versions()) _VERSIONS.update(uploader.get_versions())

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import re import re
import base64 import base64
@ -41,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services from . import constants, logger, helper, services
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, kobo_sync_status
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
valid_email, check_username valid_email, check_username
from .gdriveutils import is_gdrive_ready, gdrive_support from .gdriveutils import is_gdrive_ready, gdrive_support
@ -236,8 +235,12 @@ def view_configuration():
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all() .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all()
restrict_columns = calibre_db.session.query(db.Custom_Columns)\ restrict_columns = calibre_db.session.query(db.Custom_Columns)\
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all() .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all()
languages = calibre_db.speaking_language()
translations = [LC('en')] + babel.list_translations()
return render_title_template("config_view_edit.html", conf=config, readColumns=read_column, return render_title_template("config_view_edit.html", conf=config, readColumns=read_column,
restrictColumns=restrict_columns, restrictColumns=restrict_columns,
languages=languages,
translations=translations,
title=_(u"UI Configuration"), page="uiconfig") title=_(u"UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable") @admi.route("/admin/usertable")
@ -515,16 +518,12 @@ def check_valid_restricted_column(column):
return True return True
@admi.route("/admin/viewconfig", methods=["POST"]) @admi.route("/admin/viewconfig", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def update_view_configuration(): def update_view_configuration():
to_save = request.form.to_dict() to_save = request.form.to_dict()
# _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
# _config_int = lambda x: config.set_from_dictionary(to_save, x, int)
_config_string(to_save, "config_calibre_web_title") _config_string(to_save, "config_calibre_web_title")
_config_string(to_save, "config_columns_to_ignore") _config_string(to_save, "config_columns_to_ignore")
if _config_string(to_save, "config_title_regex"): if _config_string(to_save, "config_title_regex"):
@ -546,6 +545,8 @@ def update_view_configuration():
_config_int(to_save, "config_random_books") _config_int(to_save, "config_random_books")
_config_int(to_save, "config_books_per_page") _config_int(to_save, "config_books_per_page")
_config_int(to_save, "config_authors_max") _config_int(to_save, "config_authors_max")
_config_string(to_save, "config_default_language")
_config_string(to_save, "config_default_locale")
config.config_default_role = constants.selected_roles(to_save) config.config_default_role = constants.selected_roles(to_save)
@ -1208,6 +1209,7 @@ def _configuration_update_helper():
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path')) return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
_config_checkbox_int(to_save, "config_uploading") _config_checkbox_int(to_save, "config_uploading")
_config_checkbox_int(to_save, "config_unicode_filename")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case # Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse") reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
and config.config_login_type == constants.LOGIN_LDAP) and config.config_login_type == constants.LOGIN_LDAP)
@ -1310,6 +1312,8 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
log.error(error_flash) log.error(error_flash)
config.load() config.load()
flash(error_flash, category="error") flash(error_flash, category="error")
elif request.method == "POST" and not gdrive_error:
flash(_("Database Settings updated"), category="success")
return render_title_template("config_db.html", return render_title_template("config_db.html",
config=config, config=config,
@ -1428,8 +1432,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
else: else:
content.sidebar_view &= ~constants.DETAIL_RANDOM content.sidebar_view &= ~constants.DETAIL_RANDOM
old_state = content.kobo_only_shelves_sync
content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0 content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
# 1 -> 0: nothing has to be done
# 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs
# which don't have to be synced have to be removed (added to Shelf archive)
if old_state == 0 and content.kobo_only_shelves_sync == 1:
kobo_sync_status.update_on_sync_shelfs(content.id)
if to_save.get("default_language"): if to_save.get("default_language"):
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
if to_save.get("locale"): if to_save.get("locale"):
@ -1485,6 +1494,8 @@ def new_user():
else: else:
content.role = config.config_default_role content.role = config.config_default_role
content.sidebar_view = config.config_default_show content.sidebar_view = config.config_default_show
content.locale = config.config_default_locale
content.default_language = config.config_default_language
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser", languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
@ -1682,7 +1693,8 @@ def get_updater_status():
"9": _(u'Update failed:') + u' ' + _(u'Connection error'), "9": _(u'Update failed:') + u' ' + _(u'Connection error'),
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
"11": _(u'Update failed:') + u' ' + _(u'General error'), "11": _(u'Update failed:') + u' ' + _(u'General error'),
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir') "12": _(u'Update failed:') + u' ' + _(u'Update file could not be saved in temp dir'),
"13": _(u'Update failed:') + u' ' + _(u'Files could not be replaced during update')
} }
status['text'] = text status['text'] = text
updater_thread.status = 0 updater_thread.status = 0
@ -1736,6 +1748,8 @@ def ldap_import_create_user(user, user_data):
content.password = '' # dummy password which will be replaced by ldap one content.password = '' # dummy password which will be replaced by ldap one
content.email = useremail content.email = useremail
content.kindle_mail = kindlemail content.kindle_mail = kindlemail
content.default_language = config.config_default_language
content.locale = config.config_default_locale
content.role = config.config_default_role content.role = config.config_default_role
content.sidebar_view = config.config_default_show content.sidebar_view = config.config_default_show
content.allowed_tags = config.config_allowed_tags content.allowed_tags = config.config_allowed_tags

View File

@ -19,7 +19,6 @@
# Inspired by https://github.com/ChrisTM/Flask-CacheBust # Inspired by https://github.com/ChrisTM/Flask-CacheBust
# Uses query strings so CSS font files are found without having to resort to absolute URLs # Uses query strings so CSS font files are found without having to resort to absolute URLs
from __future__ import division, print_function, unicode_literals
import os import os
import hashlib import hashlib

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import argparse import argparse
@ -48,19 +47,6 @@ parser.add_argument('-s', metavar='user:pass', help='Sets specific username to n
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version') parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
args = parser.parse_args() args = parser.parse_args()
if sys.version_info < (3, 0):
if args.p:
args.p = args.p.decode('utf-8')
if args.g:
args.g = args.g.decode('utf-8')
if args.k:
args.k = args.k.decode('utf-8')
if args.c:
args.c = args.c.decode('utf-8')
if args.s:
args.s = args.s.decode('utf-8')
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db") gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
from . import logger, isoLanguages from . import logger, isoLanguages

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import sys import sys
import json import json
@ -91,6 +89,8 @@ class _Settings(_Base):
config_default_role = Column(SmallInteger, default=0) config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR) config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
config_default_language = Column(String(3), default="all")
config_default_locale = Column(String(2), default="en")
config_columns_to_ignore = Column(String) config_columns_to_ignore = Column(String)
config_denied_tags = Column(String, default="") config_denied_tags = Column(String, default="")
@ -133,6 +133,7 @@ class _Settings(_Base):
config_calibre = Column(String) config_calibre = Column(String)
config_rarfile_location = Column(String, default=None) config_rarfile_location = Column(String, default=None)
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD)) config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
config_unicode_filename =Column(Boolean, default=False)
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE) config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
@ -360,10 +361,6 @@ def _migrate_table(session, orm_class):
session.query(column).first() session.query(column).first()
except OperationalError as err: except OperationalError as err:
log.debug("%s: %s", column_name, err.args[0]) log.debug("%s: %s", column_name, err.args[0])
if column.default is not None:
if sys.version_info < (3, 0):
if isinstance(column.default.arg, unicode):
column.default.arg = column.default.arg.encode('utf-8')
if column.default is None: if column.default is None:
column_default = "" column_default = ""
else: else:

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
from collections import namedtuple from collections import namedtuple
@ -31,12 +30,7 @@ HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file
UPDATER_AVAILABLE = True UPDATER_AVAILABLE = True
# Base dir is parent of current file, necessary if called from different folder # Base dir is parent of current file, necessary if called from different folder
if sys.version_info < (3, 0): BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir))
BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8')
else:
BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),os.pardir))
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static') STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates') TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations') TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
@ -157,7 +151,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher') 'series_id, languages, publisher')
STABLE_VERSION = {'version': '0.6.13'} STABLE_VERSION = {'version': '0.6.14 Beta'}
NIGHTLY_VERSION = {} NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import re import re
from flask_babel import gettext as _ from flask_babel import gettext as _

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import re import re
@ -387,8 +386,6 @@ class Custom_Columns(Base):
def get_display_dict(self): def get_display_dict(self):
display_dict = ast.literal_eval(self.display) display_dict = ast.literal_eval(self.display)
if sys.version_info < (3, 0):
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
return display_dict return display_dict
@ -740,6 +737,7 @@ class CalibreDB():
self.session.connection().connection.connection.create_function("lower", 1, lcase) self.session.connection().connection.connection.create_function("lower", 1, lcase)
entries = self.session.query(database).filter(tag_filter). \ entries = self.session.query(database).filter(tag_filter). \
filter(func.lower(database.name).ilike("%" + query + "%")).all() filter(func.lower(database.name).ilike("%" + query + "%")).all()
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
return json_dumps return json_dumps

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
from datetime import datetime from datetime import datetime
import json import json
@ -40,14 +39,12 @@ try:
except ImportError: except ImportError:
have_scholar = False have_scholar = False
from babel import Locale as LC
from babel.core import UnknownLocaleError
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError from sqlalchemy.exc import OperationalError, IntegrityError
from sqlite3 import OperationalError as sqliteOperationalError from sqlite3 import OperationalError as sqliteOperationalError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
from . import config, get_locale, ub, db from . import config, get_locale, ub, db
from . import calibre_db from . import calibre_db
from .services.worker import WorkerThread from .services.worker import WorkerThread
@ -826,6 +823,8 @@ def edit_book(book_id):
if modif_date: if modif_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id)
calibre_db.session.merge(book) calibre_db.session.merge(book)
calibre_db.session.commit() calibre_db.session.commit()
if config.config_use_google_drive: if config.config_use_google_drive:
@ -1131,10 +1130,11 @@ def edit_list_book(param):
else: else:
lang_names = list() lang_names = list()
for lang in book.languages: for lang in book.languages:
try: lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale())) #try:
except UnknownLocaleError: # lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name)) #except UnknownLocaleError:
# lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json') mimetype='application/json')
elif param =='author_sort': elif param =='author_sort':

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import zipfile import zipfile
from lxml import etree from lxml import etree

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from lxml import etree from lxml import etree
from .constants import BookMeta from .constants import BookMeta

View File

@ -20,9 +20,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import sys
import hashlib import hashlib
import json import json
import tempfile import tempfile
@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import login_required from flask_login import login_required
from . import logger, gdriveutils, config, ub, calibre_db from . import logger, gdriveutils, config, ub, calibre_db, csrf
from .admin import admin_required from .admin import admin_required
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive') gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
@ -118,6 +116,7 @@ def revoke_watch_gdrive():
return redirect(url_for('admin.db_configuration')) return redirect(url_for('admin.db_configuration'))
@csrf.exempt
@gdrive.route("/watch/callback", methods=['GET', 'POST']) @gdrive.route("/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation(): def on_received_watch_confirmation():
if not config.config_google_drive_watch_changes_response: if not config.config_google_drive_watch_changes_response:
@ -137,10 +136,7 @@ def on_received_watch_confirmation():
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
log.debug('%r', response) log.debug('%r', response)
if response: if response:
if sys.version_info < (3, 0): dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
else:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \ if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web') tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import json import json
import shutil import shutil

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import io import io
@ -234,16 +233,14 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:-1]+u'_' value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0') value = value.replace("/", "_").replace(":", "_").strip('\0')
if use_unidecode: if use_unidecode:
value = (unidecode.unidecode(value)) if not config.config_unicode_filename:
value = (unidecode.unidecode(value))
else: else:
value = value.replace(u'§', u'SS') value = value.replace(u'§', u'SS')
value = value.replace(u'ß', u'ss') value = value.replace(u'ß', u'ss')
value = unicodedata.normalize('NFKD', value) value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile(r'[\W\s-]', re.UNICODE) re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
if isinstance(value, str): # Python3 str, Python2 unicode value = re_slugify.sub('', value)
value = re_slugify.sub('', value)
else:
value = unicode(re_slugify.sub('', value))
if replace_whitespace: if replace_whitespace:
# *+:\"/<>? are replaced by _ # *+:\"/<>? are replaced by _
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U) value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
@ -252,10 +249,7 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:128].strip() value = value[:128].strip()
if not value: if not value:
raise ValueError("Filename cannot be empty") raise ValueError("Filename cannot be empty")
if sys.version_info.major == 3: return value
return value
else:
return value.decode('utf-8')
def split_authors(values): def split_authors(values):
@ -495,10 +489,7 @@ def reset_password(user_id):
def generate_random_password(): def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8 passlen = 8
if sys.version_info < (3, 0): return "".join(s[c % len(s)] for c in os.urandom(passlen))
return "".join(s[ord(c) % len(s)] for c in os.urandom(passlen))
else:
return "".join(s[c % len(s)] for c in os.urandom(passlen))
def uniq(inpt): def uniq(inpt):
@ -712,8 +703,6 @@ def check_unrar(unrarLocation):
return _('Unrar binary file not found') return _('Unrar binary file not found')
try: try:
if sys.version_info < (3, 0):
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
unrarLocation = [unrarLocation] unrarLocation = [unrarLocation]
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware') value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
if value: if value:
@ -842,8 +831,8 @@ def get_download_link(book_id, book_format, client):
ub.update_download(book_id, int(current_user.id)) ub.update_download(book_id, int(current_user.id))
file_name = book.title file_name = book.title
if len(book.authors) > 0: if len(book.authors) > 0:
file_name = book.authors[0].name + '_' + file_name file_name = file_name + ' - ' + book.authors[0].name
file_name = get_valid_filename(file_name) file_name = get_valid_filename(file_name, replace_whitespace=False)
headers = Headers() headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % ( headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES

View File

@ -6,8 +6,6 @@
# pylint: disable=too-many-lines,bad-continuation # pylint: disable=too-many-lines,bad-continuation
from __future__ import unicode_literals
# This file is autogenerated, do NOT add, change, or delete ANY string # This file is autogenerated, do NOT add, change, or delete ANY string
# If you need help or assistance for adding a new language, please contact the project team # If you need help or assistance for adding a new language, please contact the project team
@ -7855,4 +7853,4 @@ LANGUAGE_NAMES = {
"zxx": "No linguistic content", "zxx": "No linguistic content",
"zza": "Zaza" "zza": "Zaza"
} }
} }

View File

@ -22,7 +22,6 @@
# custom jinja filters # custom jinja filters
from __future__ import division, print_function, unicode_literals
import datetime import datetime
import mimetypes import mimetypes
from uuid import uuid4 from uuid import uuid4

View File

@ -19,7 +19,6 @@
import base64 import base64
import datetime import datetime
import sys
import os import os
import uuid import uuid
from time import gmtime, strftime from time import gmtime, strftime
@ -47,7 +46,8 @@ from sqlalchemy.exc import StatementError
from sqlalchemy.sql import select from sqlalchemy.sql import select
import requests import requests
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
from .constants import sqlalchemy_version2 from .constants import sqlalchemy_version2
from .helper import get_download_link from .helper import get_download_link
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
@ -170,9 +170,12 @@ def HandleSyncRequest():
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(or_(db.Books.last_modified > sync_token.books_last_modified, .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
ub.BookShelf.date_added > sync_token.books_last_modified)) .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
.filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters()) ub.KoboSyncedBooks.book_id == None))
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters(allow_show_archived=True))
.order_by(db.Books.id) .order_by(db.Books.id)
.order_by(ub.ArchivedBook.last_modified) .order_by(ub.ArchivedBook.last_modified)
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id) .join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
@ -189,16 +192,16 @@ def HandleSyncRequest():
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified > sync_token.books_last_modified) .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
.filter(calibre_db.common_filters()) .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
.filter(db.Data.format.in_(KOBO_FORMATS)) ub.KoboSyncedBooks.book_id == None))
.order_by(db.Books.last_modified) .filter(calibre_db.common_filters())
.order_by(db.Books.id) .filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
) )
if sync_token.books_last_id > -1:
changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id)
reading_states_in_new_entitlements = [] reading_states_in_new_entitlements = []
if sqlalchemy_version2: if sqlalchemy_version2:
@ -206,6 +209,7 @@ def HandleSyncRequest():
else: else:
books = changed_entries.limit(SYNC_ITEM_LIMIT) books = changed_entries.limit(SYNC_ITEM_LIMIT)
for book in books: for book in books:
kobo_sync_status.add_synced_books(book.Books.id)
formats = [data.format for data in book.Books.data] formats = [data.format for data in book.Books.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
@ -263,11 +267,11 @@ def HandleSyncRequest():
entries = calibre_db.session.execute(changed_entries).all() entries = calibre_db.session.execute(changed_entries).all()
book_count = len(entries) book_count = len(entries)
else: else:
entries = changed_entries.all() #entries = changed_entries.all()
book_count = changed_entries.count() book_count = changed_entries.count()
# last entry: # last entry:
books_last_id = entries[-1].Books.id or -1 if book_count else -1 # sync_cont = entries[-1].Books.id or -1 if book_count else -1
log.debug("Remaining books to Sync: {}".format(book_count))
# generate reading state data # generate reading state data
changed_reading_states = ub.session.query(ub.KoboReadingState) changed_reading_states = ub.session.query(ub.KoboReadingState)
@ -305,7 +309,7 @@ def HandleSyncRequest():
sync_token.books_last_modified = new_books_last_modified sync_token.books_last_modified = new_books_last_modified
sync_token.archive_last_modified = new_archived_last_modified sync_token.archive_last_modified = new_archived_last_modified
sync_token.reading_state_last_modified = new_reading_state_last_modified sync_token.reading_state_last_modified = new_reading_state_last_modified
sync_token.books_last_id = books_last_id # sync_token.books_last_id = books_last_id
return generate_sync_response(sync_token, sync_results, book_count) return generate_sync_response(sync_token, sync_results, book_count)
@ -330,7 +334,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync"] = "continue" extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers) sync_token.to_headers(extra_headers)
log.debug("Kobo Sync Content: {}".format(sync_results)) # log.debug("Kobo Sync Content: {}".format(sync_results))
response = make_response(jsonify(sync_results), extra_headers) response = make_response(jsonify(sync_results), extra_headers)
return response return response
@ -483,10 +487,7 @@ def get_metadata(book):
metadata.update(get_author(book)) metadata.update(get_author(book))
if get_series(book): if get_series(book):
if sys.version_info < (3, 0): name = get_series(book)
name = get_series(book).encode("utf-8")
else:
name = get_series(book)
metadata["Series"] = { metadata["Series"] = {
"Name": get_series(book), "Name": get_series(book),
"Number": get_seriesindex(book), # ToDo Check int() ? "Number": get_seriesindex(book), # ToDo Check int() ?
@ -497,7 +498,7 @@ def get_metadata(book):
return metadata return metadata
@csrf.exempt
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"]) @kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
@requires_kobo_auth @requires_kobo_auth
# Creates a Shelf with the given items, and returns the shelf's uuid. # Creates a Shelf with the given items, and returns the shelf's uuid.
@ -532,6 +533,7 @@ def HandleTagCreate():
return make_response(jsonify(str(shelf.uuid)), 201) return make_response(jsonify(str(shelf.uuid)), 201)
@csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"]) @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
@requires_kobo_auth @requires_kobo_auth
def HandleTagUpdate(tag_id): def HandleTagUpdate(tag_id):
@ -587,6 +589,7 @@ def add_items_to_shelf(items, shelf):
return items_unknown_to_calibre return items_unknown_to_calibre
@csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
def HandleTagAddItem(tag_id): def HandleTagAddItem(tag_id):
@ -616,6 +619,7 @@ def HandleTagAddItem(tag_id):
return make_response('', 201) return make_response('', 201)
@csrf.exempt
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
def HandleTagRemoveItem(tag_id): def HandleTagRemoveItem(tag_id):
@ -757,7 +761,7 @@ def create_kobo_tag(shelf):
) )
return {"Tag": tag} return {"Tag": tag}
@csrf.exempt
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"]) @kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
@requires_kobo_auth @requires_kobo_auth
def HandleStateRequest(book_uuid): def HandleStateRequest(book_uuid):
@ -932,6 +936,7 @@ def TopLevelEndpoint():
return make_response(jsonify({})) return make_response(jsonify({}))
@csrf.exempt
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"]) @kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
@requires_kobo_auth @requires_kobo_auth
def HandleBookDeletionRequest(book_uuid): def HandleBookDeletionRequest(book_uuid):
@ -958,6 +963,7 @@ def HandleBookDeletionRequest(book_uuid):
# TODO: Implement the following routes # TODO: Implement the following routes
@csrf.exempt
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"]) @kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
def HandleUnimplementedRequest(dummy=None): def HandleUnimplementedRequest(dummy=None):
log.debug("Unimplemented Library Request received: %s", request.base_url) log.debug("Unimplemented Library Request received: %s", request.base_url)
@ -965,6 +971,7 @@ def HandleUnimplementedRequest(dummy=None):
# TODO: Implement the following routes # TODO: Implement the following routes
@csrf.exempt
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"]) @kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
@kobo.route("/v1/user/profile", methods=["GET", "POST"]) @kobo.route("/v1/user/profile", methods=["GET", "POST"])
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"]) @kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
@ -975,6 +982,7 @@ def HandleUserRequest(dummy=None):
return redirect_or_proxy_request() return redirect_or_proxy_request()
@csrf.exempt
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"]) @kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"]) @kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"]) @kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
@ -1008,6 +1016,7 @@ def make_calibre_web_auth_response():
) )
@csrf.exempt
@kobo.route("/v1/auth/device", methods=["POST"]) @kobo.route("/v1/auth/device", methods=["POST"])
@requires_kobo_auth @requires_kobo_auth
def HandleAuthRequest(): def HandleAuthRequest():

71
cps/kobo_sync_status.py Normal file
View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_login import current_user
from . import ub
import datetime
from sqlalchemy.sql.expression import or_
def add_synced_books(book_id):
synced_book = ub.KoboSyncedBooks()
synced_book.user_id = current_user.id
synced_book.book_id = book_id
ub.session.add(synced_book)
ub.session_commit()
def remove_synced_book(book_id):
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete()
ub.session_commit()
def add_archived_books(book_id):
archived_book = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.book_id == book_id)
.first()
)
if not archived_book:
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
archived_book.is_archived = True
archived_book.last_modified = datetime.datetime.utcnow()
ub.session.merge(archived_book)
ub.session_commit()
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs
def update_on_sync_shelfs(content_id):
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
.join(ub.Shelf, ub.Shelf.user_id == content_id, isouter=True)
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
.filter(ub.KoboSyncedBooks.user_id == content_id).all())
for b in books_to_archive:
add_archived_books(b.book_id)
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete()
ub.session_commit()
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter(
ub.Shelf.kobo_sync == 0).all()
for a in shelfs_to_archive:
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id))
ub.session_commit()

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os import os
import sys import sys
import inspect import inspect

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ComicVine api document: https://comicvine.gamespot.com/api/documentation
import requests
from cps.services.Metadata import Metadata
class ComicVine(Metadata):
__name__ = "ComicVine"
__id__ = "comicvine"
def search(self, query, __):
val = list()
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
if self.active:
headers = {
'User-Agent': 'Not Evil Browser'
}
result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
for r in result.json()['results']:
seriesTitle = r['volume'].get('name', "")
if r.get('store_date'):
dateFomers = r.get('store_date')
else:
dateFomers = r.get('date_added')
v = dict()
v['id'] = r['id']
v['title'] = seriesTitle + " #" + r.get('issue_number', "0") + " - " + ( r.get('name', "") or "")
v['authors'] = r.get('authors', [])
v['description'] = r.get('description', "")
v['publisher'] = ""
v['publishedDate'] = dateFomers
v['tags'] = ["Comics", seriesTitle]
v['rating'] = 0
v['series'] = seriesTitle
v['cover'] = r['image'].get('original_url')
v['source'] = {
"id": self.__id__,
"description": "ComicVine Books",
"link": "https://comicvine.gamespot.com/"
}
v['url'] = r.get('site_detail_url', "")
val.append(v)
return val

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Google Books api document: https://developers.google.com/books/docs/v1/using
import requests
from cps.services.Metadata import Metadata
class Google(Metadata):
__name__ = "Google"
__id__ = "google"
def search(self, query, __):
if self.active:
val = list()
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
for r in result.json()['items']:
v = dict()
v['id'] = r['id']
v['title'] = r['volumeInfo']['title']
v['authors'] = r['volumeInfo'].get('authors', [])
v['description'] = r['volumeInfo'].get('description', "")
v['publisher'] = r['volumeInfo'].get('publisher', "")
v['publishedDate'] = r['volumeInfo'].get('publishedDate', "")
v['tags'] = r['volumeInfo'].get('categories', [])
v['rating'] = r['volumeInfo'].get('averageRating', 0)
if r['volumeInfo'].get('imageLinks'):
v['cover'] = r['volumeInfo']['imageLinks']['thumbnail'].replace("http://", "https://")
else:
v['cover'] = "/../../../static/generic_cover.jpg"
v['source'] = {
"id": self.__id__,
"description": "Google Books",
"link": "https://books.google.com/"}
v['url'] = "https://books.google.com/books?id=" + r['id']
val.append(v)
return val

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from scholarly import scholarly
from cps.services.Metadata import Metadata
class scholar(Metadata):
__name__ = "Google Scholar"
__id__ = "googlescholar"
def search(self, query, generic_cover=""):
val = list()
if self.active:
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
i = 0
for publication in scholar_gen:
v = dict()
v['id'] = "1234" # publication['bib'].get('title')
v['title'] = publication['bib'].get('title')
v['authors'] = publication['bib'].get('author', [])
v['description'] = publication['bib'].get('abstract', "")
v['publisher'] = publication['bib'].get('venue', "")
if publication['bib'].get('pub_year'):
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
else:
v['publishedDate'] = ""
v['tags'] = ""
v['ratings'] = 0
v['series'] = ""
v['cover'] = generic_cover
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
v['source'] = {
"id": self.__id__,
"description": "Google Scholar",
"link": "https://scholar.google.com/"
}
val.append(v)
i += 1
if (i >= 10):
break
return val

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
from __future__ import division, print_function, unicode_literals
from flask import session from flask import session
try: try:

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
from __future__ import division, print_function, unicode_literals
import json import json
from functools import wraps from functools import wraps

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import datetime import datetime
from functools import wraps from functools import wraps
@ -433,16 +432,17 @@ def feed_languagesindex():
if current_user.filter_language() == u"all": if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
else: else:
try: #try:
cur_l = LC.parse(current_user.filter_language()) # cur_l = LC.parse(current_user.filter_language())
except UnknownLocaleError: #except UnknownLocaleError:
cur_l = None # cur_l = None
languages = calibre_db.session.query(db.Languages).filter( languages = calibre_db.session.query(db.Languages).filter(
db.Languages.lang_code == current_user.filter_language()).all() db.Languages.lang_code == current_user.filter_language()).all()
if cur_l: languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
languages[0].name = cur_l.get_language_name(get_locale()) #if cur_l:
else: # languages[0].name = cur_l.get_language_name(get_locale())
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name) #else:
# languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(languages)) len(languages))
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination) return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
@ -536,11 +536,10 @@ def feed_search(term):
def check_auth(username, password): def check_auth(username, password):
if sys.version_info.major == 3: try:
try: username = username.encode('windows-1252')
username = username.encode('windows-1252') except UnicodeEncodeError:
except UnicodeEncodeError: username = username.encode('utf-8')
username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
username.decode('utf-8').lower()).first() username.decode('utf-8').lower()).first()
if bool(user and check_password_hash(str(user.password), password)): if bool(user and check_password_hash(str(user.password), password)):

View File

@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from math import ceil from math import ceil

View File

@ -27,7 +27,6 @@
# http://flask.pocoo.org/snippets/62/ # http://flask.pocoo.org/snippets/62/
from __future__ import division, print_function, unicode_literals
try: try:
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
except ImportError: except ImportError:

View File

@ -36,8 +36,6 @@
# #
# Inspired by http://flask.pocoo.org/snippets/35/ # Inspired by http://flask.pocoo.org/snippets/35/
from __future__ import division, print_function, unicode_literals
class ReverseProxied(object): class ReverseProxied(object):
"""Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the

117
cps/search_metadata.py Normal file
View File

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import importlib
import sys
import inspect
import datetime
import concurrent.futures
from flask import Blueprint, request, Response, url_for
from flask_login import current_user
from flask_login import login_required
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import constants, logger, ub
from cps.services.Metadata import Metadata
meta = Blueprint('metadata', __name__)
log = logger.create()
new_list = list()
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))
for f in modules:
if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith('__init__.py'):
a = os.path.basename(f)[:-3]
try:
importlib.import_module("cps.metadata_provider." + a)
new_list.append(a)
except ImportError:
log.error("Import error for metadata source: {}".format(a))
pass
def list_classes(provider_list):
classes = list()
for element in provider_list:
for name, obj in inspect.getmembers(sys.modules["cps.metadata_provider." + element]):
if inspect.isclass(obj) and name != "Metadata" and issubclass(obj, Metadata):
classes.append(obj())
return classes
cl = list_classes(new_list)
@meta.route("/metadata/provider")
@login_required
def metadata_provider():
active = current_user.view_settings.get('metadata', {})
provider = list()
for c in cl:
ac = active.get(c.__id__, True)
provider.append({"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__})
return Response(json.dumps(provider), mimetype='application/json')
@meta.route("/metadata/provider", methods=['POST'])
@meta.route("/metadata/provider/<prov_name>", methods=['POST'])
@login_required
def metadata_change_active_provider(prov_name):
new_state = request.get_json()
active = current_user.view_settings.get('metadata', {})
active[new_state['id']] = new_state['value']
current_user.view_settings['metadata'] = active
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except (InvalidRequestError, OperationalError):
log.error("Invalid request received: {}".format(request))
return "Invalid request", 400
if "initial" in new_state and prov_name:
for c in cl:
if c.__id__ == prov_name:
data = c.search(new_state.get('query', ""))
break
return Response(json.dumps(data), mimetype='application/json')
return ""
@meta.route("/metadata/search", methods=['POST'])
@login_required
def metadata_search():
query = request.form.to_dict().get('query')
data = list()
active = current_user.view_settings.get('metadata', {})
if query:
static_cover = url_for('static', filename='generic_cover.jpg')
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
meta = {executor.submit(c.search, query, static_cover): c for c in cl if active.get(c.__id__, True)}
for future in concurrent.futures.as_completed(meta):
data.extend(future.result())
return Response(json.dumps(data), mimetype='application/json')

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import errno import errno

27
cps/services/Metadata.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Metadata():
__name__ = "Generic"
def __init__(self):
self.active = True
def set_status(self, state):
self.active = state

View File

@ -35,10 +35,7 @@ log = logger.create()
def b64encode_json(json_data): def b64encode_json(json_data):
if sys.version_info < (3, 0): return b64encode(json.dumps(json_data).encode())
return b64encode(json.dumps(json_data))
else:
return b64encode(json.dumps(json_data).encode())
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2. # Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
@ -85,8 +82,8 @@ class SyncToken:
"books_last_created": {"type": "string"}, "books_last_created": {"type": "string"},
"archive_last_modified": {"type": "string"}, "archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"}, "reading_state_last_modified": {"type": "string"},
"tags_last_modified": {"type": "string"}, "tags_last_modified": {"type": "string"}
"books_last_id": {"type": "integer", "optional": True} # "books_last_id": {"type": "integer", "optional": True}
}, },
} }
@ -97,8 +94,8 @@ class SyncToken:
books_last_modified=datetime.min, books_last_modified=datetime.min,
archive_last_modified=datetime.min, archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min, reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min, tags_last_modified=datetime.min
books_last_id=-1 # books_last_id=-1
): # nosec ): # nosec
self.raw_kobo_store_token = raw_kobo_store_token self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created self.books_last_created = books_last_created
@ -106,7 +103,7 @@ class SyncToken:
self.archive_last_modified = archive_last_modified self.archive_last_modified = archive_last_modified
self.reading_state_last_modified = reading_state_last_modified self.reading_state_last_modified = reading_state_last_modified
self.tags_last_modified = tags_last_modified self.tags_last_modified = tags_last_modified
self.books_last_id = books_last_id # self.books_last_id = books_last_id
@staticmethod @staticmethod
def from_headers(headers): def from_headers(headers):
@ -141,12 +138,12 @@ class SyncToken:
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified") archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified") reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified") tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
books_last_id = data_json["books_last_id"] # books_last_id = data_json["books_last_id"]
except TypeError: except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.") log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token) return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
except KeyError: #except KeyError:
books_last_id = -1 # books_last_id = -1
return SyncToken( return SyncToken(
raw_kobo_store_token=raw_kobo_store_token, raw_kobo_store_token=raw_kobo_store_token,
@ -155,7 +152,7 @@ class SyncToken:
archive_last_modified=archive_last_modified, archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_last_modified, reading_state_last_modified=reading_state_last_modified,
tags_last_modified=tags_last_modified, tags_last_modified=tags_last_modified,
books_last_id=books_last_id #books_last_id=books_last_id
) )
def set_kobo_store_header(self, store_headers): def set_kobo_store_header(self, store_headers):
@ -179,16 +176,16 @@ class SyncToken:
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified), "archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified), "reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified), "tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
"books_last_id":self.books_last_id #"books_last_id":self.books_last_id
}, },
} }
return b64encode_json(token) return b64encode_json(token)
def __str__(self): def __str__(self):
return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token, return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
self.books_last_created, self.books_last_created,
self.books_last_modified, self.books_last_modified,
self.archive_last_modified, self.archive_last_modified,
self.reading_state_last_modified, self.reading_state_last_modified,
self.tags_last_modified, self.tags_last_modified)
self.books_last_id) #self.books_last_id)

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from .. import logger from .. import logger

View File

@ -1,4 +1,21 @@
from __future__ import print_function # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path import os.path
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request from google.auth.transport.requests import Request

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import time import time
from functools import reduce from functools import reduce

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import base64 import base64
from flask_simpleldap import LDAP, LDAPException from flask_simpleldap import LDAP, LDAPException

View File

@ -1,5 +1,21 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import threading import threading
import abc import abc
import uuid import uuid
@ -205,10 +221,13 @@ class CalibreTask:
# By default, we're good to clean a task if it's "Done" # By default, we're good to clean a task if it's "Done"
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL) return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
@progress.setter '''@progress.setter
def progress(self, x): def progress(self, x):
# todo: throw error if outside of [0,1] if x > 1:
self._progress = x x = 1
if x < 0:
x = 0
self._progress = x'''
def _handleError(self, error_message): def _handleError(self, error_message):
self.stat = STAT_FAIL self.stat = STAT_FAIL

View File

@ -20,8 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
from datetime import datetime from datetime import datetime

View File

@ -35,7 +35,7 @@ body {
float: left; float: left;
width: 40px; width: 40px;
height: 40px; height: 40px;
background-image: url("img/toolbar-buttons.png"); background-image: url("../../js/libs/djvu_html5/img/toolbar-buttons.png");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 500% 300%; background-size: 500% 300%;
} }
@ -185,10 +185,10 @@ _:-ms-lang(x), .textLayer {
width: 128px; width: 128px;
height: 128px; height: 128px;
margin: -72px 0 0 -64px; margin: -72px 0 0 -64px;
background-image: url("img/status.png"); background-image: url("../../js/libs/djvu_html5/img/status.png");
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.blankImage { .blankImage {
background-image: url("img/blank.jpg"); background-image: url("../../js/libs/djvu_html5/img/blank.jpg");
} }

View File

@ -145,7 +145,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
cursor: not-allowed; cursor: not-allowed;
background-color: #eeeeee !important; background-color: #eeeeee !important;
} }
.tt-dropdown-menu { .tt-menu {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
@ -166,7 +166,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
*border-right-width: 2px; *border-right-width: 2px;
*border-bottom-width: 2px; *border-bottom-width: 2px;
} }
.tt-dropdown-menu .tt-suggestion { .tt-menu .tt-suggestion {
display: block; display: block;
padding: 3px 20px; padding: 3px 20px;
clear: both; clear: both;
@ -175,15 +175,15 @@ fieldset[disabled] .twitter-typeahead .tt-input {
color: #333333; color: #333333;
white-space: nowrap; white-space: nowrap;
} }
.tt-dropdown-menu .tt-suggestion.tt-cursor { .tt-menu .tt-suggestion.tt-cursor {
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
background-color: #f5f5f5; background-color: #f5f5f5;
color: #262626; color: #262626;
} }
.tt-dropdown-menu .tt-suggestion.tt-cursor a { .tt-menu .tt-suggestion.tt-cursor a {
color: #262626; color: #262626;
} }
.tt-dropdown-menu .tt-suggestion p { .tt-menu .tt-suggestion p {
margin: 0; margin: 0;
} }

View File

@ -123,6 +123,10 @@ table .bg-dark-danger a { color: #fff; }
flex-wrap: wrap; flex-wrap: wrap;
} }
.row-fluid.text-center {
margin-top: -20px;
}
.container-fluid img { .container-fluid img {
display: block; display: block;
max-width: 100%; max-width: 100%;
@ -166,6 +170,10 @@ table .bg-dark-danger a { color: #fff; }
box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777;
} }
.datepicker.form-control {
position: static;
}
.container-fluid .book .cover span img { .container-fluid .book .cover span img {
position: relative; position: relative;
top: 0; top: 0;
@ -322,7 +330,7 @@ table .bg-dark-danger:hover { background-color: #c9302c; }
table .bg-primary:hover { background-color: #1c5484; } table .bg-primary:hover { background-color: #1c5484; }
.block-label { display: block; } .block-label { display: block; }
.fake-input { .form-control.fake-input {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
top: 0; top: 0;

44
cps/static/css/text.css Normal file
View File

@ -0,0 +1,44 @@
body {
background: white;
}
#readmain {
position: absolute;
width: 100%;
height: 100%;
}
#area {
width: 80%;
height: 80%;
margin: 5% auto;
max-width: 1250px;
}
#area iframe {
border: none;
}
xmp, pre, plaintext {
display: block;
font-family: -moz-fixed;
white-space: pre;
margin: 1em 0;
}
#area{
overflow:hidden;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: -moz-fixed;
column-count:2;
-webkit-columns:2;
-moz-columns:2;
column-gap:20px;
-moz-column-gap:20px;
-webkit-column-gap:20px;
position:relative;
}

View File

@ -22,15 +22,17 @@ $(function() {
}); });
$("#have_read_cb").on("change", function() { $("#have_read_cb").on("change", function() {
$.post({ $.ajax({
url: this.closest("form").action, url: this.closest("form").action,
method:"post",
data: $(this).closest("form").serialize(),
error: function(response) { error: function(response) {
var data = [{type:"danger", message:response.responseText}] var data = [{type:"danger", message:response.responseText}]
$("#flash_success").remove(); $("#flash_success").remove();
$("#flash_danger").remove(); $("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) { if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) { data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' + $(".navbar").after('<div class="row-fluid text-center" >' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' + '<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>'); '</div>');
}); });

View File

@ -23,7 +23,6 @@ if ($(".tiny_editor").length) {
$(".datepicker").datepicker({ $(".datepicker").datepicker({
format: "yyyy-mm-dd", format: "yyyy-mm-dd",
language: language
}).on("change", function () { }).on("change", function () {
// Show localized date over top of the standard YYYY-MM-DD date // Show localized date over top of the standard YYYY-MM-DD date
var pubDate; var pubDate;
@ -47,90 +46,20 @@ $(".datepicker_delete").click(function() {
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
and returns the completions it gets from the bloodhound engine prefixed. and returns the completions it gets from the bloodhound engine prefixed.
*/ */
function prefixedSource(prefix, query, cb, bhAdapter) { function prefixedSource(prefix, query, cb, source) {
bhAdapter(query, function(retArray) { function async(retArray) {
retArray = retArray || [];
var matches = []; var matches = [];
for (var i = 0; i < retArray.length; i++) { for (var i = 0; i < retArray.length; i++) {
var obj = {name : prefix + retArray[i].name}; var obj = {name : prefix + retArray[i].name};
matches.push(obj); matches.push(obj);
} }
cb(matches); cb(matches);
}); }
source.search(query, cb, async);
} }
var authors = new Bloodhound({
name: "authors",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: getPath() + "/get_authors_json?q=%QUERY"
}
});
var series = new Bloodhound({
name: "series",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: function queryTokenizer(query) {
return [query];
},
remote: {
url: getPath() + "/get_series_json?q=",
replace: function replace(url, query) {
return url + encodeURIComponent(query);
}
}
});
var tags = new Bloodhound({
name: "tags",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: function queryTokenizer(query) {
var tokens = query.split(",");
tokens = [tokens[tokens.length - 1].trim()];
return tokens;
},
remote: {
url: getPath() + "/get_tags_json?q=%QUERY"
}
});
var languages = new Bloodhound({
name: "languages",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: function queryTokenizer(query) {
return [query];
},
remote: {
url: getPath() + "/get_languages_json?q=",
replace: function replace(url, query) {
return url + encodeURIComponent(query);
}
}
});
var publishers = new Bloodhound({
name: "publisher",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: getPath() + "/get_publishers_json?q=%QUERY"
}
});
function sourceSplit(query, cb, split, source) { function sourceSplit(query, cb, split, source) {
var bhAdapter = source.ttAdapter();
var tokens = query.split(split); var tokens = query.split(split);
var currentSource = tokens[tokens.length - 1].trim(); var currentSource = tokens[tokens.length - 1].trim();
@ -145,84 +74,148 @@ function sourceSplit(query, cb, split, source) {
for (var i = 0; i < tokens.length; i++) { for (var i = 0; i < tokens.length; i++) {
prefix += tokens[i].trim() + newSplit; prefix += tokens[i].trim() + newSplit;
} }
prefixedSource(prefix, currentSource, cb, bhAdapter); prefixedSource(prefix, currentSource, cb, source);
} }
var promiseAuthors = authors.initialize(); var authors = new Bloodhound({
promiseAuthors.done(function() { name: "authors",
$("#bookAuthor").typeahead( identify: function(obj) { return obj.name; },
{ datumTokenizer: function datumTokenizer(datum) {
highlight: true, minLength: 1, return [datum.name];
hint: true },
}, { queryTokenizer: Bloodhound.tokenizers.whitespace,
name: "authors", remote: {
displayKey: "name", url: getPath() + "/get_authors_json?q=%QUERY",
source: function source(query, cb) { wildcard: '%QUERY',
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&") },
}
}
);
}); });
var promiseSeries = series.initialize(); $(".form-group #bookAuthor").typeahead(
promiseSeries.done(function() { {
$("#series").typeahead( highlight: true,
{ minLength: 1,
highlight: true, minLength: 0, hint: true
hint: true }, {
}, { name: "authors",
name: "series", display: 'name',
displayKey: "name", source: function source(query, cb, asyncResults) {
source: series.ttAdapter() return sourceSplit(query, cb, "&", authors);
} }
); }
);
var series = new Bloodhound({
name: "series",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
// queryTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: function queryTokenizer(query) {
return [query];
},
remote: {
url: getPath() + "/get_series_json?q=%QUERY",
wildcard: '%QUERY',
/*replace: function replace(url, query) {
return url + encodeURIComponent(query);
}*/
}
});
$(".form-group #series").typeahead(
{
highlight: true,
minLength: 0,
hint: true
}, {
name: "series",
displayKey: "name",
source: series
}
);
var tags = new Bloodhound({
name: "tags",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: function queryTokenizer(query) {
var tokens = query.split(",");
tokens = [tokens[tokens.length - 1].trim()];
return tokens;
},
remote: {
url: getPath() + "/get_tags_json?q=%QUERY",
wildcard: '%QUERY'
}
}); });
var promiseTags = tags.initialize(); $(".form-group #tags").typeahead(
promiseTags.done(function() { {
$("#tags").typeahead( highlight: true,
{ minLength: 0,
highlight: true, minLength: 0, hint: true
hint: true }, {
}, { name: "tags",
name: "tags", display: "name",
displayKey: "name", source: function source(query, cb, asyncResults) {
source: function source(query, cb) { return sourceSplit(query, cb, ",", tags);
return sourceSplit(query, cb, ",", tags);
}
} }
); }
);
var languages = new Bloodhound({
name: "languages",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: function queryTokenizer(query) {
return [query];
},
remote: {
url: getPath() + "/get_languages_json?q=%QUERY",
wildcard: '%QUERY'
/*replace: function replace(url, query) {
return url + encodeURIComponent(query);
}*/
}
}); });
var promiseLanguages = languages.initialize(); $(".form-group #languages").typeahead(
promiseLanguages.done(function() { {
$("#languages").typeahead( highlight: true, minLength: 0,
{ hint: true
highlight: true, minLength: 0, }, {
hint: true name: "languages",
}, { display: "name",
name: "languages", source: function source(query, cb, asyncResults) {
displayKey: "name", return sourceSplit(query, cb, ",", languages);
source: function source(query, cb) {
return sourceSplit(query, cb, ",", languages); //(",")
}
} }
); }
);
var publishers = new Bloodhound({
name: "publisher",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: getPath() + "/get_publishers_json?q=%QUERY",
wildcard: '%QUERY'
}
}); });
var promisePublishers = publishers.initialize(); $(".form-group #publisher").typeahead(
promisePublishers.done(function() { {
$("#publisher").typeahead( highlight: true, minLength: 0,
{ hint: true
highlight: true, minLength: 0, }, {
hint: true name: "publishers",
}, { displayKey: "name",
name: "publishers", source: publishers
displayKey: "name", }
source: publishers.ttAdapter() );
}
);
});
$("#search").on("change input.typeahead:selected", function(event) { $("#search").on("change input.typeahead:selected", function(event) {
if (event.target.type === "search" && event.target.tagName === "INPUT") { if (event.target.type === "search" && event.target.tagName === "INPUT") {
@ -269,3 +262,4 @@ $("#xchange").click(function () {
$("#book_title").val($("#bookAuthor").val()); $("#book_title").val($("#bookAuthor").val());
$("#bookAuthor").val(title); $("#bookAuthor").val(title);
}); });

View File

@ -14,380 +14,134 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* /* global _, i18nMsg, tinymce, getPath */
* Get Metadata from Douban Books api and Google Books api and ComicVine
* Google Books api document: https://developers.google.com/books/docs/v1/using
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
* ComicVine api document: https://comicvine.gamespot.com/api/documentation
*/
/* global _, i18nMsg, tinymce */
var dbResults = [];
var ggResults = [];
var cvResults = [];
var gsResults = [];
$(function () { $(function () {
var msg = i18nMsg; var msg = i18nMsg;
var douban = "https://api.douban.com"; var keyword = ""
var dbSearch = "/v2/book/search";
var dbDone = 0;
var google = "https://www.googleapis.com"; var templates = {
var ggSearch = "/books/v1/volumes"; bookResult: _.template(
var ggDone = 0; $("#template-book-result").html()
)
};
var comicvine = "https://comicvine.gamespot.com"; function populateForm (book) {
var cvSearch = "/api/search/"; tinymce.get("description").setContent(book.description);
var cvDone = 0; var uniqueTags = [];
$.each(book.tags, function(i, el) {
var googlescholar = window.location.href.split('/admin/book')[0]; if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
var gsSearch = "/scholarsearch/"
var gsDone = 0;
var showFlag = 0;
var templates = {
bookResult: _.template(
$("#template-book-result").html()
)
};
function populateForm (book) {
tinymce.get("description").setContent(book.description);
var uniqueTags = [];
$.each(book.tags, function(i, el) {
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
});
var ampSeparatedAuthors = (book.authors || []).join(" & ");
$("#bookAuthor").val(ampSeparatedAuthors);
$("#book_title").val(book.title);
$("#tags").val(uniqueTags.join(","));
$("#rating").data("rating").setValue(Math.round(book.rating));
if(book.cover !== null){
$(".cover img").attr("src", book.cover);
$("#cover_url").val(book.cover);
}
$("#pubdate").val(book.publishedDate);
$("#publisher").val(book.publisher);
if (typeof book.series !== "undefined") {
$("#series").val(book.series);
}
}
function showResult () {
showFlag++;
if (showFlag === 1) {
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
}
if ((ggDone === 3 || (ggDone === 1 && ggResults.length === 0)) &&
(dbDone === 3 || (dbDone === 1 && dbResults.length === 0)) &&
(cvDone === 3 || (cvDone === 1 && cvResults.length === 0)) &&
(gsDone === 3 || (gsDone === 1 && gsResults.length === 0))) {
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
return;
}
function formatDate (date) {
var d = new Date(date),
month = "" + (d.getMonth() + 1),
day = "" + d.getDate(),
year = d.getFullYear();
if (month.length < 2) {
month = "0" + month;
}
if (day.length < 2) {
day = "0" + day;
}
return [year, month, day].join("-");
}
function generateID (title) {
return title.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0).toString().substr(0,12);
}
if (ggResults.length > 0) {
if (ggDone < 2) {
ggResults.forEach(function(result) {
var book = {
id: result.id,
title: result.volumeInfo.title,
authors: result.volumeInfo.authors || [],
description: result.volumeInfo.description || "",
publisher: result.volumeInfo.publisher || "",
publishedDate: result.volumeInfo.publishedDate || "",
tags: result.volumeInfo.categories || [],
rating: result.volumeInfo.averageRating || 0,
cover: result.volumeInfo.imageLinks ?
result.volumeInfo.imageLinks.thumbnail : location + "/../../../static/generic_cover.jpg",
url: "https://books.google.com/books?id=" + result.id,
source: {
id: "google",
description: "Google Books",
url: "https://books.google.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
}); });
ggDone = 2; var ampSeparatedAuthors = (book.authors || []).join(" & ");
} else { $("#bookAuthor").val(ampSeparatedAuthors);
ggDone = 3; $("#book_title").val(book.title);
} $("#tags").val(uniqueTags.join(","));
} $("#rating").data("rating").setValue(Math.round(book.rating));
if(book.cover !== null){
if (gsResults.length > 0) { $(".cover img").attr("src", book.cover);
if (gsDone < 2) { $("#cover_url").val(book.cover);
gsResults.forEach(function(result) { }
var book = { $("#pubdate").val(book.publishedDate);
id: generateID(result.bib.title), $("#publisher").val(book.publisher);
title: result.bib.title, if (typeof book.series !== "undefined") {
authors: result.bib.author || [], $("#series").val(book.series);
description: result.bib.abstract || "",
publisher: result.bib.venue || "",
publishedDate: result.bib.pub_year ? result.bib.pub_year+"-01-01" : "",
tags: [],
rating: 0,
series: "",
cover: null,
url: result.pub_url || result.eprint_url || "",
source: {
id: "googlescholar",
description: "Google Scholar",
link: "https://scholar.google.com/"
}
}
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
gsDone = 2;
}
else {
gsDone = 3;
}
}
if (dbResults.length > 0) {
if (dbDone < 2) {
dbResults.forEach(function(result) {
var seriesTitle = "";
if (result.series) {
seriesTitle = result.series.title;
}
var dateFomers = result.pubdate.split("-");
var publishedYear = parseInt(dateFomers[0], 10);
var publishedMonth = parseInt(dateFomers[1], 10);
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
publishedDate = formatDate(publishedDate);
var book = {
id: result.id,
title: result.title,
authors: result.author || [],
description: result.summary,
publisher: result.publisher || "",
publishedDate: publishedDate || "",
tags: result.tags.map(function(tag) {
return tag.title.toLowerCase().replace(/,/g, "_");
}),
rating: result.rating.average || 0,
series: seriesTitle || "",
cover: result.image,
url: "https://book.douban.com/subject/" + result.id,
source: {
id: "douban",
description: "Douban Books",
url: "https://book.douban.com/"
}
};
if (book.rating > 0) {
book.rating /= 2;
}
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
dbDone = 2;
} else {
dbDone = 3;
}
}
if (cvResults.length > 0) {
if (cvDone < 2) {
cvResults.forEach(function(result) {
var seriesTitle = "";
if (result.volume.name) {
seriesTitle = result.volume.name;
}
var dateFomers = "";
if (result.store_date) {
dateFomers = result.store_date.split("-");
} else {
dateFomers = result.date_added.split("-");
}
var publishedYear = parseInt(dateFomers[0], 10);
var publishedMonth = parseInt(dateFomers[1], 10);
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
publishedDate = formatDate(publishedDate);
var book = {
id: result.id,
title: seriesTitle + " #" + ("00" + result.issue_number).slice(-3) + " - " + result.name,
authors: result.author || [],
description: result.description,
publisher: "",
publishedDate: publishedDate || "",
tags: ["Comics", seriesTitle],
rating: 0,
series: seriesTitle || "",
cover: result.image.original_url,
url: result.site_detail_url,
source: {
id: "comicvine",
description: "ComicVine Books",
url: "https://comicvine.gamespot.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
cvDone = 2;
} else {
cvDone = 3;
}
}
}
function ggSearchBook (title) {
$.ajax({
url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
type: "GET",
dataType: "jsonp",
jsonp: "callback",
success: function success(data) {
if ("items" in data) {
ggResults = data.items;
} }
},
complete: function complete() {
ggDone = 1;
showResult();
$("#show-google").trigger("change");
}
});
}
function dbSearchBook (title) {
var apikey = "054022eaeae0b00e0fc068c0c0a2102a";
$.ajax({
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
type: "GET",
dataType: "jsonp",
jsonp: "callback",
success: function success(data) {
dbResults = data.books;
},
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
},
complete: function complete() {
dbDone = 1;
showResult();
$("#show-douban").trigger("change");
}
});
}
function cvSearchBook (title) {
var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6";
title = encodeURIComponent(title);
$.ajax({
url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp",
type: "GET",
dataType: "jsonp",
jsonp: "json_callback",
success: function success(data) {
cvResults = data.results;
},
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
},
complete: function complete() {
cvDone = 1;
showResult();
$("#show-comics").trigger("change");
}
});
}
function gsSearchBook (title) {
$.ajax({
url: googlescholar + gsSearch + title.replace(/\s+/gm,'+'),
type: "GET",
dataType: "json",
success: function success(data) {
gsResults = data;
},
complete: function complete() {
gsDone = 1;
showResult();
$("#show-googlescholar").trigger("change");
}
})
}
function doSearch (keyword) {
showFlag = 0;
dbDone = ggDone = cvDone = 0;
dbResults = [];
ggResults = [];
cvResults = [];
gsResults = [];
$("#meta-info").text(msg.loading);
if (keyword) {
dbSearchBook(keyword);
ggSearchBook(keyword);
cvSearchBook(keyword);
gsSearchBook(keyword);
} }
}
$("#meta-search").on("submit", function (e) { function doSearch (keyword) {
e.preventDefault(); if (keyword) {
var keyword = $("#keyword").val(); $("#meta-info").text(msg.loading);
if (keyword) { $.ajax({
doSearch(keyword); url: getPath() + "/metadata/search",
type: "POST",
data: {"query": keyword},
dataType: "json",
success: function success(data) {
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
data.forEach(function(book) {
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
},
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
},
});
}
} }
});
$("#get_meta").click(function () { function populate_provider() {
var bookTitle = $("#book_title").val(); $("#metadata_provider").empty();
if (bookTitle) { $.ajax({
$("#keyword").val(bookTitle); url: getPath() + "/metadata/provider",
doSearch(bookTitle); type: "get",
dataType: "json",
success: function success(data) {
data.forEach(function(provider) {
var checked = "";
if (provider.active) {
checked = "checked";
}
var $provider_button = '<input type="checkbox" id="show-' + provider.name + '" class="pill" data-initial="' + provider.initial + '" data-control="' + provider.id + '" ' + checked + '><label for="show-' + provider.name + '">' + provider.name + ' <span class="glyphicon glyphicon-ok"></span></label>'
$("#metadata_provider").append($provider_button);
});
},
});
} }
});
$(document).on("change", ".pill", function () {
var element = $(this);
var id = element.data("control");
var initial = element.data("initial");
var val = element.prop('checked');
var params = {id : id, value: val};
if (!initial) {
params['initial'] = initial;
params['query'] = keyword;
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/metadata/provider/" + id,
data: JSON.stringify(params),
success: function success(data) {
element.data("initial", "true");
data.forEach(function(book) {
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
}
});
});
$("#meta-search").on("submit", function (e) {
e.preventDefault();
keyword = $("#keyword").val();
$('.pill').each(function(){
// console.log($(this).data('control'));
$(this).data("initial", $(this).prop('checked'));
// console.log($(this).data('initial'));
});
doSearch(keyword);
});
$("#get_meta").click(function () {
populate_provider();
var bookTitle = $("#book_title").val();
$("#keyword").val(bookTitle);
keyword = bookTitle;
doSearch(bookTitle);
});
$("#metaModal").on("show.bs.modal", function(e) {
$(e.relatedTarget).one('focus', function (e) {
$(this).blur();
});
});
}); });

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
function getPath() { function getPath() {
var jsFileLocation = $("script[src*=jquery]").attr("src"); // the js file path var jsFileLocation = $("script[src*=jquery]").attr("src"); // the js file path
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
@ -113,6 +112,14 @@ $("#btn-upload").change(function() {
$("#form-upload").submit(); $("#form-upload").submit();
}); });
$("#form-upload").uploadprogress({
redirect_url: getPath() + "/", //"{{ url_for('web.index')}}",
uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}",
modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}",
modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}",
modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}"
});
$(document).ready(function() { $(document).ready(function() {
var inp = $('#query').first() var inp = $('#query').first()
if (inp.length) { if (inp.length) {
@ -181,7 +188,7 @@ $("#delete_confirm").click(function() {
if (item.format != "") { if (item.format != "") {
$("button[data-delete-format='"+item.format+"']").addClass('hidden'); $("button[data-delete-format='"+item.format+"']").addClass('hidden');
} }
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' + $( ".navbar" ).after( '<div class="row-fluid text-center" >' +
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' + '<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
'</div>'); '</div>');
@ -224,6 +231,16 @@ $(function() {
var preFilters = $.Callbacks(); var preFilters = $.Callbacks();
$.ajaxPrefilter(preFilters.fire); $.ajaxPrefilter(preFilters.fire);
// equip all post requests with csrf_token
var csrftoken = $("input[name='csrf_token']").val();
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
});
function restartTimer() { function restartTimer() {
$("#spinner").addClass("hidden"); $("#spinner").addClass("hidden");
$("#RestartDialog").modal("hide"); $("#RestartDialog").modal("hide");
@ -246,16 +263,15 @@ $(function() {
function updateTimer() { function updateTimer() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../get_updater_status", url: getPath() + "/get_updater_status",
success: function success(data) { success: function success(data) {
// console.log(data.status);
$("#DialogContent").html(updateText[data.status]); $("#DialogContent").html(updateText[data.status]);
if (data.status > 6) { if (data.status > 6) {
cleanUp(); cleanUp();
} }
}, },
error: function error() { error: function error() {
$("#DialogContent").html(updateText[7]); $("#DialogContent").html(updateText[11]);
cleanUp(); cleanUp();
}, },
timeout: 2000 timeout: 2000
@ -446,12 +462,11 @@ $(function() {
$.ajax({ $.ajax({
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: { start: "True"}, data: { start: "True" },
url: window.location.pathname + "/../../get_updater_status", url: getPath() + "/get_updater_status",
success: function success(data) { success: function success(data) {
updateText = data.text; updateText = data.text;
$("#DialogContent").html(updateText[data.status]); $("#DialogContent").html(updateText[data.status]);
// console.log(data.status);
updateTimerID = setInterval(updateTimer, 2000); updateTimerID = setInterval(updateTimer, 2000);
} }
}); });
@ -554,7 +569,7 @@ $(function() {
function handle_response(data) { function handle_response(data) {
if (!jQuery.isEmptyObject(data)) { if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) { data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' + $(".navbar").after('<div class="row-fluid text-center">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' + '<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>'); '</div>');
}); });
@ -579,7 +594,7 @@ $(function() {
method:"post", method:"post",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/simulatedbchange", url: window.location.pathname + "/../../ajax/simulatedbchange",
data: {config_calibre_dir: $("#config_calibre_dir").val()}, data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
success: function success(data) { success: function success(data) {
if ( data.change ) { if ( data.change ) {
if ( data.valid ) { if ( data.valid ) {
@ -652,10 +667,10 @@ $(function() {
var folder = target.data("folderonly"); var folder = target.data("folderonly");
var filter = target.data("filefilter"); var filter = target.data("filefilter");
$("#element_selected").text(path); $("#element_selected").text(path);
$("#file_confirm")[0].attributes["data-link"].value = target.data("link"); $("#file_confirm").data("link", target.data("link"));
$("#file_confirm")[0].attributes["data-folderonly"].value = (typeof folder === 'undefined') ? false : true; $("#file_confirm").data("folderonly", (typeof folder === 'undefined') ? false : true);
$("#file_confirm")[0].attributes["data-filefilter"].value = (typeof filter === 'undefined') ? "" : filter; $("#file_confirm").data("filefilter", (typeof filter === 'undefined') ? "" : filter);
$("#file_confirm")[0].attributes["data-newfile"].value = target.data("newfile"); $("#file_confirm").data("newfile", target.data("newfile"));
fillFileTable(path,"dir", folder, filter); fillFileTable(path,"dir", folder, filter);
}); });
@ -669,7 +684,7 @@ $(function() {
var folder = $(file_confirm).data("folderonly"); var folder = $(file_confirm).data("folderonly");
var filter = $(file_confirm).data("filefilter"); var filter = $(file_confirm).data("filefilter");
var newfile = $(file_confirm).data("newfile"); var newfile = $(file_confirm).data("newfile");
if (newfile !== 'undefined') { if (newfile !== "") {
$("#element_selected").text(path + $("#new_file".text())); $("#element_selected").text(path + $("#new_file".text()));
} else { } else {
$("#element_selected").text(path); $("#element_selected").text(path);
@ -715,7 +730,7 @@ $(function() {
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../ajax/view", url: getPath() + "/ajax/view",
data: "{\"series\": {\"series_view\": \""+ view +"\"}}", data: "{\"series\": {\"series_view\": \""+ view +"\"}}",
success: function success() { success: function success() {
location.reload(); location.reload();

View File

@ -0,0 +1,21 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2021 Ozzieisaacs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var DJVU_CONTEXT = {
background: "#666",
uiHideDelay: 1500,
};

View File

@ -0,0 +1,86 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2021 Ozzieisaacs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$(document).ready(function() {
//to int
$("#area").width($("#area").width());
$("#content").width($("#content").width());
//bind text
$("#content").load($("#readmain").data('load'), function(textStr) {
$(this).height($(this).parent().height()*0.95);
$(this).text(textStr);
});
//keybind
$(document).keydown(function(event){
if(event.keyCode == 37){
prevPage();
}
if(event.keyCode == 39){
nextPage();
}
});
//click
$( "#left" ).click(function() {
prevPage();
});
$( "#right" ).click(function() {
nextPage();
});
$("#readmain").swipe( {
swipeRight:function() {
prevPage();
},
swipeLeft:function() {
nextPage();
},
});
//bind mouse
$(window).bind('DOMMouseScroll mousewheel', function(event) {
var delta = 0;
if (event.originalEvent.wheelDelta) {
delta = event.originalEvent.wheelDelta;
} else if (event.originalEvent.detail) {
delta = event.originalEvent.detail*-1;
}
if (delta >= 0) {
prevPage();
} else {
nextPage();
}
});
//page animate
var origwidth = $("#content")[0].getBoundingClientRect().width;
var gap = 20;
function prevPage() {
if($("#content").offset().left > 0) {
return;
}
leftoff = $("#content").offset().left;
leftoff = leftoff+origwidth+gap;
$("#content").offset({left:leftoff});
}
function nextPage() {
leftoff = $("#content").offset().left;
leftoff = leftoff-origwidth-gap;
if (leftoff + $("#content")[0].scrollWidth < 0) {
return;
}
$("#content").offset({left:leftoff});
}
});

View File

@ -0,0 +1,36 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2017-2021 jkrehm, OzzieIsaacs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function () {
// Poll the server to check if the user has authenticated
var t = setInterval(function () {
$.post(getPath() + "/ajax/verify_token", { token: $("#verify_url").data("token") })
.done(function(response) {
if (response.status === 'success') {
// Wait a tick so cookies are updated
setTimeout(function () {
window.location.href = getPath() + '/';
}, 0);
}
})
.fail(function (xhr) {
clearInterval(t);
var response = JSON.parse(xhr.responseText);
alert(response.message);
});
}, 5000);
})()

View File

@ -35,6 +35,7 @@ function sendData(path) {
var form = document.createElement("form"); var form = document.createElement("form");
form.setAttribute("method", "post"); form.setAttribute("method", "post");
form.setAttribute("action", path); form.setAttribute("action", path);
// form.setAttribute("csrf_token", );
for (counter = 0;counter < maxElements;counter++) { for (counter = 0;counter < maxElements;counter++) {
tmp[counter] = elements[counter].getAttribute("id"); tmp[counter] = elements[counter].getAttribute("id");
@ -44,6 +45,10 @@ function sendData(path) {
hiddenField.setAttribute("value", String(counter + 1)); hiddenField.setAttribute("value", String(counter + 1));
form.appendChild(hiddenField); form.appendChild(hiddenField);
} }
$("<input type='hidden'/>")
.attr("name", "csrf_token").val($("input[name='csrf_token']").val())
.appendTo(form);
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} }

View File

@ -85,7 +85,7 @@ $(function() {
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/mergebooks", url: window.location.pathname + "/../ajax/mergebooks",
data: JSON.stringify({"Merge_books":selections}), data: JSON.stringify({"Merge_books":selections}),
success: function success() { success: function success() {
$("#books-table").bootstrapTable("refresh"); $("#books-table").bootstrapTable("refresh");
@ -104,7 +104,7 @@ $(function() {
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/simulatemerge", url: window.location.pathname + "/../ajax/simulatemerge",
data: JSON.stringify({"Merge_books":selections}), data: JSON.stringify({"Merge_books":selections}),
success: function success(booTitles) { success: function success(booTitles) {
$.each(booTitles.from, function(i, item) { $.each(booTitles.from, function(i, item) {
@ -121,7 +121,7 @@ $(function() {
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/xchange", url: window.location.pathname + "/../ajax/xchange",
data: JSON.stringify({"xchange":selections}), data: JSON.stringify({"xchange":selections}),
success: function success() { success: function success() {
$("#books-table").bootstrapTable("refresh"); $("#books-table").bootstrapTable("refresh");
@ -161,10 +161,11 @@ $(function() {
$("#books-table").bootstrapTable({ $("#books-table").bootstrapTable({
sidePagination: "server", sidePagination: "server",
pageList: "[10, 25, 50, 100]",
queryParams: queryParams, queryParams: queryParams,
pagination: true, pagination: true,
paginationLoop: false, paginationLoop: false,
paginationDetailHAlign: " hidden", paginationDetailHAlign: "right",
paginationHAlign: "left", paginationHAlign: "left",
idField: "id", idField: "id",
uniqueId: "id", uniqueId: "id",
@ -187,7 +188,7 @@ $(function() {
$.ajax({ $.ajax({
method:"get", method:"get",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id, url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id,
success: function success(data) { success: function success(data) {
var key = Object.keys(data)[0]; var key = Object.keys(data)[0];
$("#books-table").bootstrapTable("updateCellByUniqueId", { $("#books-table").bootstrapTable("updateCellByUniqueId", {
@ -215,7 +216,7 @@ $(function() {
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/table_settings", url: window.location.pathname + "/../ajax/table_settings",
data: "{" + st + "}", data: "{" + st + "}",
}); });
}, },
@ -769,7 +770,7 @@ function handleListServerResponse (data) {
$("#flash_danger").remove(); $("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) { if (!jQuery.isEmptyObject(data)) {
data.forEach(function(item) { data.forEach(function(item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' + $(".navbar").after('<div class="row-fluid text-center">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' + '<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>'); '</div>');
}); });

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
import subprocess import subprocess
@ -33,13 +32,8 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
if key in quotes: if key in quotes:
command[key] = '"' + element + '"' command[key] = '"' + element + '"'
exc_command = " ".join(command) exc_command = " ".join(command)
if sys.version_info < (3, 0):
exc_command = exc_command.encode(sys.getfilesystemencoding())
else: else:
if sys.version_info < (3, 0): exc_command = [x for x in command]
exc_command = [x.encode(sys.getfilesystemencoding()) for x in command]
else:
exc_command = [x for x in command]
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec

View File

@ -1,4 +1,21 @@
from __future__ import division, print_function, unicode_literals # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
import os import os
import re import re
@ -161,8 +178,6 @@ class TaskConvert(CalibreTask):
while True: while True:
nextline = p.stdout.readlines() nextline = p.stdout.readlines()
nextline = [x.strip('\n') for x in nextline if x != '\n'] nextline = [x.strip('\n') for x in nextline if x != '\n']
if sys.version_info < (3, 0):
nextline = [x.decode('utf-8') for x in nextline]
for line in nextline: for line in nextline:
log.debug(line) log.debug(line)
if p.poll() is not None: if p.poll() is not None:
@ -207,10 +222,6 @@ class TaskConvert(CalibreTask):
while p.poll() is None: while p.poll() is None:
nextline = p.stdout.readline() nextline = p.stdout.readline()
if os.name == 'nt' and sys.version_info < (3, 0):
nextline = nextline.decode('windows-1252')
elif os.name == 'posix' and sys.version_info < (3, 0):
nextline = nextline.decode('utf-8')
log.debug(nextline.strip('\r\n')) log.debug(nextline.strip('\r\n'))
# parse progress string from calibre-converter # parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline) progress = re.search(r"(\d+)%\s.*", nextline)
@ -224,8 +235,6 @@ class TaskConvert(CalibreTask):
calibre_traceback = p.stderr.readlines() calibre_traceback = p.stderr.readlines()
error_message = "" error_message = ""
for ele in calibre_traceback: for ele in calibre_traceback:
if sys.version_info < (3, 0):
ele = ele.decode('utf-8')
log.debug(ele.strip('\n')) log.debug(ele.strip('\n'))
if not ele.startswith('Traceback') and not ele.startswith(' File'): if not ele.startswith('Traceback') and not ele.startswith(' File'):
error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n')) error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n'))

View File

@ -1,11 +1,27 @@
from __future__ import division, print_function, unicode_literals # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
import os import os
import smtplib import smtplib
import threading import threading
import socket import socket
import mimetypes import mimetypes
import base64
try: try:
from StringIO import StringIO from StringIO import StringIO
@ -162,17 +178,11 @@ class TaskEmail(CalibreTask):
log.debug_or_exception(ex) log.debug_or_exception(ex)
self._handleError(u'Error sending e-mail: {}'.format(ex)) self._handleError(u'Error sending e-mail: {}'.format(ex))
def send_standard_email(self, msg): def send_standard_email(self, msg):
use_ssl = int(self.settings.get('mail_use_ssl', 0)) use_ssl = int(self.settings.get('mail_use_ssl', 0))
timeout = 600 # set timeout to 5mins timeout = 600 # set timeout to 5mins
# redirect output to logfile on python2 on python3 debugoutput is caught with overwritten # on python3 debugoutput is caught with overwritten _print_debug function
# _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
log.debug("Start sending e-mail") log.debug("Start sending e-mail")
if use_ssl == 2: if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
@ -198,9 +208,6 @@ class TaskEmail(CalibreTask):
self._handleSuccess() self._handleSuccess()
log.debug("E-mail send successfully") log.debug("E-mail send successfully")
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
def send_gmail_email(self, message): def send_gmail_email(self, message):
return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message) return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message)
@ -218,7 +225,6 @@ class TaskEmail(CalibreTask):
self.asyncSMTP = None self.asyncSMTP = None
self._progress = x self._progress = x
@classmethod @classmethod
def _get_attachment(cls, bookpath, filename): def _get_attachment(cls, bookpath, filename):
"""Get file as MIMEBase message""" """Get file as MIMEBase message"""

View File

@ -1,4 +1,20 @@
from __future__ import division, print_function, unicode_literals # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime from datetime import datetime
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS

View File

@ -186,6 +186,7 @@
</table> </table>
{% if feature_support['updater'] %} {% if feature_support['updater'] %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>

View File

@ -36,8 +36,8 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img"> <span class="img" title="{{entry.title|safe}}">
<img title="{{author.name|safe}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" /> <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -23,20 +23,21 @@
{% if source_formats|length > 0 and conversion_formats|length > 0 %} {% if source_formats|length > 0 and conversion_formats|length > 0 %}
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4> <div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
<form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm"> <form class="padded-bottom" action="{{ url_for('editbook.convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group"> <div class="form-group">
<div class="text-left"> <div class="text-left">
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label> <label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
<select class="form-control" name="book_format_from" id="book_format_from"> <select class="form-control" name="book_format_from" id="book_format_from">
<option disabled selected value> -- {{_('select an option')}} -- </option> <option disabled selected value>-- {{_('select an option')}} --</option>
{% for format in source_formats %} {% for format in source_formats %}
<option>{{format|upper}} </option> <option>{{format|upper}}</option>
{% endfor %} {% endfor %}
</select> </select>
<label class="control-label" for="book_format_to">{{_('Convert to:')}}</label> <label class="control-label" for="book_format_to">{{_('Convert to:')}}</label>
<select class="form-control" name="book_format_to" id="book_format_to"> <select class="form-control" name="book_format_to" id="book_format_to">
<option disabled selected value> -- {{_('select an option')}} -- </option> <option disabled selected value>-- {{_('select an option')}} --</option>
{% for format in conversion_formats %} {% for format in conversion_formats %}
<option>{{format|upper}} </option> <option>{{format|upper}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -48,6 +49,7 @@
</div> </div>
<form role="form" action="{{ url_for('editbook.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm"> <form role="form" action="{{ url_for('editbook.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-sm-9 col-xs-12"> <div class="col-sm-9 col-xs-12">
<div class="form-group"> <div class="form-group">
<label for="book_title">{{_('Book Title')}}</label> <label for="book_title">{{_('Book Title')}}</label>
@ -56,11 +58,11 @@
<div class="text-center"> <div class="text-center">
<button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button> <button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button>
</div> </div>
<div id="author_div" class="form-group">
<div class="form-group">
<label for="bookAuthor">{{_('Author')}}</label> <label for="bookAuthor">{{_('Author')}}</label>
<input type="text" class="form-control typeahead" name="author_name" id="bookAuthor" value="{{' & '.join(authors)}}" autocomplete="off"> <input type="text" class="form-control typeahead" name="author_name" id="bookAuthor" value="{{' & '.join(authors)}}" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">{{_('Description')}}</label> <label for="description">{{_('Description')}}</label>
<textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea> <textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
@ -111,8 +113,8 @@
{% endif %} {% endif %}
<label for="pubdate">{{_('Published Date')}}</label> <label for="pubdate">{{_('Published Date')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<input type="text" style="position: static;" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}"> <input type="text" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}">
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdate}}{% endif %}"> <input type="text" class="form-control fake-input hidden" id="fake_pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdate}}{% endif %}">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="pubdate_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button> <button type="button" id="pubdate_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
</span> </span>
@ -156,11 +158,11 @@
{% if c.datatype == 'datetime' %} {% if c.datatype == 'datetime' %}
<div class="input-group"> <div class="input-group">
<input type="text" style="position: static;" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" <input type="text" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %} {% if book['custom_column_' ~ c.id]|length > 0 %}
value="{% if book['custom_column_' ~ c.id][0].value %}{{ book['custom_column_' ~ c.id][0].value|formatdateinput}}{% endif %}" value="{% if book['custom_column_' ~ c.id][0].value %}{{ book['custom_column_' ~ c.id][0].value|formatdateinput}}{% endif %}"
{% endif %}> {% endif %}>
<input type="text" style="position: absolute;" class="fake_custom_column_{{ c.id }} form-control fake-input hidden" id="fake_pubdate" <input type="text" class="fake_custom_column_{{ c.id }} form-control fake-input hidden" id="fake_pubdate_{{ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %} {% if book['custom_column_' ~ c.id]|length > 0 %}
value="{% if book['custom_column_' ~ c.id][0].value %}{{book['custom_column_' ~ c.id][0].value|formatdate}}{% endif %}" value="{% if book['custom_column_' ~ c.id][0].value %}{{book['custom_column_' ~ c.id][0].value|formatdate}}{% endif %}"
{% endif %}> {% endif %}>
@ -232,7 +234,7 @@
<form class="padded-bottom" id="meta-search"> <form class="padded-bottom" id="meta-search">
<div class="input-group"> <div class="input-group">
<label class="sr-only" for="keyword">{{_('Keyword')}}</label> <label class="sr-only" for="keyword">{{_('Keyword')}}</label>
<input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_(" Search keyword ")}}"> <input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_("Search keyword")}}">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-primary" id="do-search">{{_("Search")}}</button> <button type="submit" class="btn btn-primary" id="do-search">{{_("Search")}}</button>
</span> </span>
@ -241,25 +243,12 @@
<div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div> <div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="text-center padded-bottom"> <div class="text-center padded-bottom" id="metadata_provider">
<input type="checkbox" id="show-douban" class="pill" data-control="douban" checked>
<label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label>
<input type="checkbox" id="show-google" class="pill" data-control="google" checked>
<label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label>
<input type="checkbox" id="show-comics" class="pill" data-control="comicvine" checked>
<label for="show-comics">ComicVine <span class="glyphicon glyphicon-ok"></span></label>
<input type="checkbox" id="show-googlescholar" class="pill" data-control="googlescholar" checked>
<label for="show-googlescholar">Google Scholar <span class="glyphicon glyphicon-ok"></span></label>
</div> </div>
<div id="meta-info"> <div id="meta-info">
{{_("Loading...")}} {{_("Loading...")}}
</div> </div>
<ul id="book-list" class="media-list"></ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
@ -275,7 +264,7 @@
<img class="pull-left img-responsive" <img class="pull-left img-responsive"
data-toggle="modal" data-toggle="modal"
data-target="#metaModal" data-target="#metaModal"
src="<%= cover || "{{ url_for('static', filename='img/academicpaper.svg') }}" %>" src="<%= cover || "{{ url_for('static', filename='img/academicpaper.svg') }}" %>"
alt="Cover" alt="Cover"
> >
<div class="media-body"> <div class="media-body">

View File

@ -20,6 +20,7 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<h2 class="{{page}}">{{_(title)}}</h2> <h2 class="{{page}}">{{_(title)}}</h2>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="row form-group"> <div class="row form-group">
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div> <div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div>
@ -78,11 +79,11 @@
<p></p> <p></p>
<div class="text-left">{{_('Books with Title will be merged from:')}}</div> <div class="text-left">{{_('Books with Title will be merged from:')}}</div>
<p></p> <p></p>
<div class=text-left" id="merge_from"></div> <div class="text-left" id="merge_from"></div>
<p></p> <p></p>
<div class="text-left">{{_('Into Book with Title:')}}</div> <div class="text-left">{{_('Into Book with Title:')}}</div>
<p></p> <p></p>
<div class=text-left" id="merge_to"></div> <div class="text-left" id="merge_to"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal"> <input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
@ -98,5 +99,9 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/table.js') }}"></script> <script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block flash %} {% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;"> <div id="spinning_success" class="row-fluid text-center" style="display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div> <div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div> </div>
{% endblock %} {% endblock %}
@ -8,6 +8,7 @@
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off"> <form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label> <label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required input-group"> <div class="form-group required input-group">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">

View File

@ -1,6 +1,6 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block flash %} {% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;"> <div id="spinning_success" class="row-fluid text-center" style="display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div> <div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div> </div>
{% endblock %} {% endblock %}
@ -8,7 +8,8 @@
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off"> <form role="form" method="POST" autocomplete="off">
<div class="panel-group col-md-10 col-lg-6"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="panel-group col-md-10 col-lg-8">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">
@ -31,7 +32,7 @@
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span> </span>
</div> </div>
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label> <label for="config_keyfile" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
@ -94,6 +95,10 @@
</div> </div>
<div id="collapsefour" class="panel-collapse collapse"> <div id="collapsefour" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<div class="form-group">
<input type="checkbox" id="config_unicode_filename" name="config_unicode_filename" {% if config.config_unicode_filename %}checked{% endif %}>
<label for="config_unicode_filename">{{_('Convert non-English characters in title and author while saving to disk')}}</label>
</div>
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}> <input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
<label for="config_uploading">{{_('Enable Uploads')}}</label> <label for="config_uploading">{{_('Enable Uploads')}}</label>

Some files were not shown because too many files have changed in this diff Show More