diff --git a/README.md b/README.md
index bddc4257..0fc6447d 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ Please note that running the above install command can fail on some versions of
## 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:
diff --git a/cps.py b/cps.py
index 46eb1440..ab9896ce 100755
--- a/cps.py
+++ b/cps.py
@@ -17,18 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import absolute_import, division, print_function, unicode_literals
import sys
import os
# Insert local directories into path
-if sys.version_info < (3, 0):
- 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__.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'))
+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
@@ -42,13 +37,14 @@ from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
from cps.remotelogin import remotelogin
+from cps.search_metadata import meta
from cps.error_handler import init_errorhandler
try:
from cps.kobo import kobo, get_kobo_activated
from cps.kobo_auth import kobo_auth
kobo_available = get_kobo_activated()
-except ImportError:
+except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False
try:
@@ -70,6 +66,7 @@ def main():
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(remotelogin)
+ app.register_blueprint(meta)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
if kobo_available:
diff --git a/cps/MyLoginManager.py b/cps/MyLoginManager.py
new file mode 100644
index 00000000..7c916bd5
--- /dev/null
+++ b/cps/MyLoginManager.py
@@ -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 .
+
+
+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
diff --git a/cps/__init__.py b/cps/__init__.py
index 3b100907..a6305f0a 100644
--- a/cps/__init__.py
+++ b/cps/__init__.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
import mimetypes
@@ -29,7 +28,7 @@ from babel import Locale as LC
from babel import negotiate_locale
from babel.core import UnknownLocaleError
from flask import Flask, request, g
-from flask_login import LoginManager
+from .MyLoginManager import MyLoginManager
from flask_babel import Babel
from flask_principal import Principal
@@ -43,6 +42,12 @@ try:
except ImportError:
lxml_present = False
+try:
+ from flask_wtf.csrf import CSRFProtect
+ wtf_present = True
+except ImportError:
+ wtf_present = False
+
mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
@@ -70,11 +75,17 @@ app.config.update(
)
-lm = LoginManager()
+lm = MyLoginManager()
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong'
+if wtf_present:
+ csrf = CSRFProtect()
+ csrf.init_app(app)
+else:
+ csrf = None
+
ub.init_db(cli.settingspath)
# pylint: disable=no-member
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" ***')
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
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)
- # 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'):
cache_buster.init_cache_busting(app)
@@ -139,7 +150,6 @@ def create_app():
def get_locale():
# if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None)
- # user = None
if user is not None and hasattr(user, "locale"):
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
@@ -160,6 +170,7 @@ def get_timezone():
user = getattr(g, 'user', None)
return user.timezone if user else None
+
from .updater import Updater
updater_thread = Updater()
updater_thread.start()
diff --git a/cps/about.py b/cps/about.py
index 66c0ef40..f3c8b95f 100644
--- a/cps/about.py
+++ b/cps/about.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import platform
import sqlite3
@@ -29,6 +28,10 @@ from collections import OrderedDict
import babel, pytz, requests, sqlalchemy
import werkzeug, flask, flask_login, flask_principal, jinja2
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 .render_template import render_title_template
@@ -75,6 +78,7 @@ _VERSIONS = OrderedDict(
Flask=flask.__version__,
Flask_Login=flask_loginVersion,
Flask_Principal=flask_principal.__version__,
+ Flask_WTF=flaskwtf_version,
Werkzeug=werkzeug.__version__,
Babel=babel.__version__,
Jinja2=jinja2.__version__,
@@ -84,14 +88,14 @@ _VERSIONS = OrderedDict(
SQLite=sqlite3.sqlite_version,
iso639=isoLanguages.__version__,
pytz=pytz.__version__,
- Unidecode = unidecode_version,
- Scholarly = scholarly_version,
- Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
- python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
- Goodreads = u'installed' if bool(services.goodreads_support) else None,
- jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
- flask_dance = flask_danceVersion,
- greenlet = greenlet_Version
+ Unidecode=unidecode_version,
+ Scholarly=scholarly_version,
+ Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
+ python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
+ Goodreads=u'installed' if bool(services.goodreads_support) else None,
+ jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
+ flask_dance=flask_danceVersion,
+ greenlet=greenlet_Version
)
_VERSIONS.update(uploader.get_versions())
diff --git a/cps/admin.py b/cps/admin.py
index 7d2a1e8b..d811e4fb 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
import re
import base64
@@ -41,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text
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, \
valid_email, check_username
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()
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
.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,
restrictColumns=restrict_columns,
+ languages=languages,
+ translations=translations,
title=_(u"UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable")
@@ -515,16 +518,12 @@ def check_valid_restricted_column(column):
return True
-
@admi.route("/admin/viewconfig", methods=["POST"])
@login_required
@admin_required
def update_view_configuration():
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_columns_to_ignore")
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_books_per_page")
_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)
@@ -1208,6 +1209,7 @@ def _configuration_update_helper():
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_unicode_filename")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
and config.config_login_type == constants.LOGIN_LDAP)
@@ -1310,6 +1312,8 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
log.error(error_flash)
config.load()
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",
config=config,
@@ -1428,8 +1432,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
else:
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
-
+ # 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"):
content.default_language = to_save["default_language"]
if to_save.get("locale"):
@@ -1485,6 +1494,8 @@ def new_user():
else:
content.role = config.config_default_role
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,
languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check)
@@ -1682,7 +1693,8 @@ def get_updater_status():
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
"11": _(u'Update failed:') + u' ' + _(u'General error'),
- "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
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.email = useremail
content.kindle_mail = kindlemail
+ content.default_language = config.config_default_language
+ content.locale = config.config_default_locale
content.role = config.config_default_role
content.sidebar_view = config.config_default_show
content.allowed_tags = config.config_allowed_tags
diff --git a/cps/cache_buster.py b/cps/cache_buster.py
index 8c521fe1..9619d605 100644
--- a/cps/cache_buster.py
+++ b/cps/cache_buster.py
@@ -19,7 +19,6 @@
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
# 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 hashlib
diff --git a/cps/cli.py b/cps/cli.py
index 3bb08c1f..3685e8e2 100644
--- a/cps/cli.py
+++ b/cps/cli.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
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')
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")
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
diff --git a/cps/comic.py b/cps/comic.py
index 462c11f0..b094c60f 100644
--- a/cps/comic.py
+++ b/cps/comic.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
from . import logger, isoLanguages
diff --git a/cps/config_sql.py b/cps/config_sql.py
index f3e82e7e..0b45059d 100644
--- a/cps/config_sql.py
+++ b/cps/config_sql.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
-from __future__ import division, print_function, unicode_literals
import os
import sys
import json
@@ -91,6 +89,8 @@ class _Settings(_Base):
config_default_role = Column(SmallInteger, default=0)
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_denied_tags = Column(String, default="")
@@ -133,6 +133,7 @@ class _Settings(_Base):
config_calibre = Column(String)
config_rarfile_location = Column(String, default=None)
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)
@@ -360,10 +361,6 @@ def _migrate_table(session, orm_class):
session.query(column).first()
except OperationalError as err:
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:
column_default = ""
else:
diff --git a/cps/constants.py b/cps/constants.py
index 5aaf6cc8..367bc29d 100644
--- a/cps/constants.py
+++ b/cps/constants.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
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
# 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)).decode('utf-8')
-else:
- 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))
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
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, '
'series_id, languages, publisher')
-STABLE_VERSION = {'version': '0.6.13'}
+STABLE_VERSION = {'version': '0.6.14 Beta'}
NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$'
diff --git a/cps/converter.py b/cps/converter.py
index 6b0f22e4..fcbabbfc 100644
--- a/cps/converter.py
+++ b/cps/converter.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
import re
from flask_babel import gettext as _
diff --git a/cps/db.py b/cps/db.py
index cfb2ed7c..d8709335 100644
--- a/cps/db.py
+++ b/cps/db.py
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
import re
@@ -387,8 +386,6 @@ class Custom_Columns(Base):
def get_display_dict(self):
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
@@ -740,6 +737,7 @@ class CalibreDB():
self.session.connection().connection.connection.create_function("lower", 1, lcase)
entries = self.session.query(database).filter(tag_filter). \
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])
return json_dumps
diff --git a/cps/editbooks.py b/cps/editbooks.py
index dd506fa6..d3e1f01e 100644
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
from datetime import datetime
import json
@@ -40,14 +39,12 @@ try:
except ImportError:
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_babel import gettext as _
from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError
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 calibre_db
from .services.worker import WorkerThread
@@ -826,6 +823,8 @@ def edit_book(book_id):
if modif_date:
book.last_modified = datetime.utcnow()
+ kobo_sync_status.remove_synced_book(edited_books_id)
+
calibre_db.session.merge(book)
calibre_db.session.commit()
if config.config_use_google_drive:
@@ -1131,10 +1130,11 @@ def edit_list_book(param):
else:
lang_names = list()
for lang in book.languages:
- try:
- lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
- except UnknownLocaleError:
- lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
+ lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
+ #try:
+ # lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
+ #except UnknownLocaleError:
+ # lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json')
elif param =='author_sort':
diff --git a/cps/epub.py b/cps/epub.py
index 998dbfa6..cbbdcbbd 100644
--- a/cps/epub.py
+++ b/cps/epub.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
import zipfile
from lxml import etree
diff --git a/cps/fb2.py b/cps/fb2.py
index af4a29a7..21586736 100644
--- a/cps/fb2.py
+++ b/cps/fb2.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
from lxml import etree
from .constants import BookMeta
diff --git a/cps/gdrive.py b/cps/gdrive.py
index c0764015..267607de 100644
--- a/cps/gdrive.py
+++ b/cps/gdrive.py
@@ -20,9 +20,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
-import sys
import hashlib
import json
import tempfile
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
from flask_babel import gettext as _
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
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
@@ -118,6 +116,7 @@ def revoke_watch_gdrive():
return redirect(url_for('admin.db_configuration'))
+@csrf.exempt
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation():
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'])
log.debug('%r', response)
if response:
- if sys.version_info < (3, 0):
- dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
- else:
- dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
+ dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py
index c63b0393..d3277814 100644
--- a/cps/gdriveutils.py
+++ b/cps/gdriveutils.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
import json
import shutil
diff --git a/cps/helper.py b/cps/helper.py
index d3e0dbbb..df7019e6 100644
--- a/cps/helper.py
+++ b/cps/helper.py
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
import io
@@ -234,16 +233,14 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0')
if use_unidecode:
- value = (unidecode.unidecode(value))
+ if not config.config_unicode_filename:
+ value = (unidecode.unidecode(value))
else:
value = value.replace(u'§', u'SS')
value = value.replace(u'ß', u'ss')
value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
- if isinstance(value, str): # Python3 str, Python2 unicode
- value = re_slugify.sub('', value)
- else:
- value = unicode(re_slugify.sub('', value))
+ value = re_slugify.sub('', value)
if replace_whitespace:
# *+:\"/<>? are replaced by _
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()
if not value:
raise ValueError("Filename cannot be empty")
- if sys.version_info.major == 3:
- return value
- else:
- return value.decode('utf-8')
+ return value
def split_authors(values):
@@ -495,10 +489,7 @@ def reset_password(user_id):
def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8
- if sys.version_info < (3, 0):
- 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))
+ return "".join(s[c % len(s)] for c in os.urandom(passlen))
def uniq(inpt):
@@ -712,8 +703,6 @@ def check_unrar(unrarLocation):
return _('Unrar binary file not found')
try:
- if sys.version_info < (3, 0):
- unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
unrarLocation = [unrarLocation]
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
if value:
@@ -842,8 +831,8 @@ def get_download_link(book_id, book_format, client):
ub.update_download(book_id, int(current_user.id))
file_name = book.title
if len(book.authors) > 0:
- file_name = book.authors[0].name + '_' + file_name
- file_name = get_valid_filename(file_name)
+ file_name = file_name + ' - ' + book.authors[0].name
+ file_name = get_valid_filename(file_name, replace_whitespace=False)
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py
index 35d9f0a7..6c66a583 100644
--- a/cps/isoLanguages.py
+++ b/cps/isoLanguages.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
-
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
diff --git a/cps/iso_language_names.py b/cps/iso_language_names.py
index d07158a0..9966ecd9 100644
--- a/cps/iso_language_names.py
+++ b/cps/iso_language_names.py
@@ -6,8 +6,6 @@
# pylint: disable=too-many-lines,bad-continuation
-from __future__ import unicode_literals
-
# 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
@@ -7855,4 +7853,4 @@ LANGUAGE_NAMES = {
"zxx": "No linguistic content",
"zza": "Zaza"
}
-}
\ No newline at end of file
+}
diff --git a/cps/jinjia.py b/cps/jinjia.py
index 554bc791..06e99141 100644
--- a/cps/jinjia.py
+++ b/cps/jinjia.py
@@ -22,7 +22,6 @@
# custom jinja filters
-from __future__ import division, print_function, unicode_literals
import datetime
import mimetypes
from uuid import uuid4
diff --git a/cps/kobo.py b/cps/kobo.py
index 6952a692..a415ec5f 100644
--- a/cps/kobo.py
+++ b/cps/kobo.py
@@ -19,7 +19,6 @@
import base64
import datetime
-import sys
import os
import uuid
from time import gmtime, strftime
@@ -47,7 +46,8 @@ from sqlalchemy.exc import StatementError
from sqlalchemy.sql import select
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 .helper import get_download_link
from .services import SyncToken as SyncToken
@@ -170,9 +170,12 @@ def HandleSyncRequest():
ub.ArchivedBook.is_archived)
changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
- .filter(or_(db.Books.last_modified > sync_token.books_last_modified,
- ub.BookShelf.date_added > sync_token.books_last_modified))
- .filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters())
+ .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
+ .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
+ 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(ub.ArchivedBook.last_modified)
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
@@ -189,16 +192,16 @@ def HandleSyncRequest():
ub.ArchivedBook.last_modified,
ub.ArchivedBook.is_archived)
changed_entries = (changed_entries
- .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
- .filter(db.Books.last_modified > sync_token.books_last_modified)
- .filter(calibre_db.common_filters())
- .filter(db.Data.format.in_(KOBO_FORMATS))
- .order_by(db.Books.last_modified)
- .order_by(db.Books.id)
+ .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
+ .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
+ .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
+ ub.KoboSyncedBooks.book_id == None))
+ .filter(calibre_db.common_filters())
+ .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 = []
if sqlalchemy_version2:
@@ -206,6 +209,7 @@ def HandleSyncRequest():
else:
books = changed_entries.limit(SYNC_ITEM_LIMIT)
for book in books:
+ kobo_sync_status.add_synced_books(book.Books.id)
formats = [data.format for data in book.Books.data]
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)
@@ -263,11 +267,11 @@ def HandleSyncRequest():
entries = calibre_db.session.execute(changed_entries).all()
book_count = len(entries)
else:
- entries = changed_entries.all()
+ #entries = changed_entries.all()
book_count = changed_entries.count()
# 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
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.archive_last_modified = new_archived_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)
@@ -330,7 +334,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync"] = "continue"
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)
return response
@@ -483,10 +487,7 @@ def get_metadata(book):
metadata.update(get_author(book))
if get_series(book):
- if sys.version_info < (3, 0):
- name = get_series(book).encode("utf-8")
- else:
- name = get_series(book)
+ name = get_series(book)
metadata["Series"] = {
"Name": get_series(book),
"Number": get_seriesindex(book), # ToDo Check int() ?
@@ -497,7 +498,7 @@ def get_metadata(book):
return metadata
-
+@csrf.exempt
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
@requires_kobo_auth
# 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)
+@csrf.exempt
@kobo.route("/v1/library/tags/", methods=["DELETE", "PUT"])
@requires_kobo_auth
def HandleTagUpdate(tag_id):
@@ -587,6 +589,7 @@ def add_items_to_shelf(items, shelf):
return items_unknown_to_calibre
+@csrf.exempt
@kobo.route("/v1/library/tags//items", methods=["POST"])
@requires_kobo_auth
def HandleTagAddItem(tag_id):
@@ -616,6 +619,7 @@ def HandleTagAddItem(tag_id):
return make_response('', 201)
+@csrf.exempt
@kobo.route("/v1/library/tags//items/delete", methods=["POST"])
@requires_kobo_auth
def HandleTagRemoveItem(tag_id):
@@ -757,7 +761,7 @@ def create_kobo_tag(shelf):
)
return {"Tag": tag}
-
+@csrf.exempt
@kobo.route("/v1/library//state", methods=["GET", "PUT"])
@requires_kobo_auth
def HandleStateRequest(book_uuid):
@@ -932,6 +936,7 @@ def TopLevelEndpoint():
return make_response(jsonify({}))
+@csrf.exempt
@kobo.route("/v1/library/", methods=["DELETE"])
@requires_kobo_auth
def HandleBookDeletionRequest(book_uuid):
@@ -958,6 +963,7 @@ def HandleBookDeletionRequest(book_uuid):
# TODO: Implement the following routes
+@csrf.exempt
@kobo.route("/v1/library/", methods=["DELETE", "GET"])
def HandleUnimplementedRequest(dummy=None):
log.debug("Unimplemented Library Request received: %s", request.base_url)
@@ -965,6 +971,7 @@ def HandleUnimplementedRequest(dummy=None):
# TODO: Implement the following routes
+@csrf.exempt
@kobo.route("/v1/user/loyalty/", methods=["GET", "POST"])
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
@@ -975,6 +982,7 @@ def HandleUserRequest(dummy=None):
return redirect_or_proxy_request()
+@csrf.exempt
@kobo.route("/v1/products//prices", methods=["GET", "POST"])
@kobo.route("/v1/products//recommendations", methods=["GET", "POST"])
@kobo.route("/v1/products//nextread", methods=["GET", "POST"])
@@ -1008,6 +1016,7 @@ def make_calibre_web_auth_response():
)
+@csrf.exempt
@kobo.route("/v1/auth/device", methods=["POST"])
@requires_kobo_auth
def HandleAuthRequest():
diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py
new file mode 100644
index 00000000..f4a66604
--- /dev/null
+++ b/cps/kobo_sync_status.py
@@ -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 .
+
+
+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()
diff --git a/cps/logger.py b/cps/logger.py
index e2747f53..5847188b 100644
--- a/cps/logger.py
+++ b/cps/logger.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import os
import sys
import inspect
diff --git a/cps/metadata_provider/comicvine.py b/cps/metadata_provider/comicvine.py
new file mode 100644
index 00000000..8f496608
--- /dev/null
+++ b/cps/metadata_provider/comicvine.py
@@ -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 .
+
+# 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
+
+
diff --git a/cps/metadata_provider/google.py b/cps/metadata_provider/google.py
new file mode 100644
index 00000000..f3d02d8e
--- /dev/null
+++ b/cps/metadata_provider/google.py
@@ -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 .
+
+# 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
+
+
diff --git a/cps/metadata_provider/scholar.py b/cps/metadata_provider/scholar.py
new file mode 100644
index 00000000..6e13c768
--- /dev/null
+++ b/cps/metadata_provider/scholar.py
@@ -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 .
+
+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
+
+
+
diff --git a/cps/oauth.py b/cps/oauth.py
index a8995180..f8e5c1fd 100644
--- a/cps/oauth.py
+++ b/cps/oauth.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-from __future__ import division, print_function, unicode_literals
from flask import session
try:
diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py
index c8cc2e3e..d9efd41e 100644
--- a/cps/oauth_bb.py
+++ b/cps/oauth_bb.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-from __future__ import division, print_function, unicode_literals
import json
from functools import wraps
diff --git a/cps/opds.py b/cps/opds.py
index e444302a..92c51d1b 100644
--- a/cps/opds.py
+++ b/cps/opds.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import datetime
from functools import wraps
@@ -433,16 +432,17 @@ def feed_languagesindex():
if current_user.filter_language() == u"all":
languages = calibre_db.speaking_language()
else:
- try:
- cur_l = LC.parse(current_user.filter_language())
- except UnknownLocaleError:
- cur_l = None
+ #try:
+ # cur_l = LC.parse(current_user.filter_language())
+ #except UnknownLocaleError:
+ # cur_l = None
languages = calibre_db.session.query(db.Languages).filter(
db.Languages.lang_code == current_user.filter_language()).all()
- if cur_l:
- languages[0].name = cur_l.get_language_name(get_locale())
- else:
- languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
+ languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
+ #if cur_l:
+ # languages[0].name = cur_l.get_language_name(get_locale())
+ #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,
len(languages))
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):
- if sys.version_info.major == 3:
- try:
- username = username.encode('windows-1252')
- except UnicodeEncodeError:
- username = username.encode('utf-8')
+ try:
+ username = username.encode('windows-1252')
+ except UnicodeEncodeError:
+ username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
username.decode('utf-8').lower()).first()
if bool(user and check_password_hash(str(user.password), password)):
diff --git a/cps/pagination.py b/cps/pagination.py
index c29717c2..7a9bfb70 100644
--- a/cps/pagination.py
+++ b/cps/pagination.py
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
from math import ceil
diff --git a/cps/redirect.py b/cps/redirect.py
index 593afd0c..d491b353 100644
--- a/cps/redirect.py
+++ b/cps/redirect.py
@@ -27,7 +27,6 @@
# http://flask.pocoo.org/snippets/62/
-from __future__ import division, print_function, unicode_literals
try:
from urllib.parse import urlparse, urljoin
except ImportError:
diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py
index 3bcbd3b7..4acb8e45 100644
--- a/cps/reverseproxy.py
+++ b/cps/reverseproxy.py
@@ -36,8 +36,6 @@
#
# Inspired by http://flask.pocoo.org/snippets/35/
-from __future__ import division, print_function, unicode_literals
-
class ReverseProxied(object):
"""Wrap the application in this middleware and configure the
diff --git a/cps/search_metadata.py b/cps/search_metadata.py
new file mode 100644
index 00000000..e837fe21
--- /dev/null
+++ b/cps/search_metadata.py
@@ -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 .
+
+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/", 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')
+
+
+
+
+
+
diff --git a/cps/server.py b/cps/server.py
index 9b79f77d..e261c50a 100644
--- a/cps/server.py
+++ b/cps/server.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
import errno
diff --git a/cps/services/Metadata.py b/cps/services/Metadata.py
new file mode 100644
index 00000000..d6e4e7d5
--- /dev/null
+++ b/cps/services/Metadata.py
@@ -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 .
+
+
+class Metadata():
+ __name__ = "Generic"
+
+ def __init__(self):
+ self.active = True
+
+ def set_status(self, state):
+ self.active = state
diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py
index cc67542c..692aaa24 100644
--- a/cps/services/SyncToken.py
+++ b/cps/services/SyncToken.py
@@ -35,10 +35,7 @@ log = logger.create()
def b64encode_json(json_data):
- if sys.version_info < (3, 0):
- return b64encode(json.dumps(json_data))
- else:
- return b64encode(json.dumps(json_data).encode())
+ return b64encode(json.dumps(json_data).encode())
# 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"},
"archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"},
- "tags_last_modified": {"type": "string"},
- "books_last_id": {"type": "integer", "optional": True}
+ "tags_last_modified": {"type": "string"}
+ # "books_last_id": {"type": "integer", "optional": True}
},
}
@@ -97,8 +94,8 @@ class SyncToken:
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
- tags_last_modified=datetime.min,
- books_last_id=-1
+ tags_last_modified=datetime.min
+ # books_last_id=-1
): # nosec
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
@@ -106,7 +103,7 @@ class SyncToken:
self.archive_last_modified = archive_last_modified
self.reading_state_last_modified = reading_state_last_modified
self.tags_last_modified = tags_last_modified
- self.books_last_id = books_last_id
+ # self.books_last_id = books_last_id
@staticmethod
def from_headers(headers):
@@ -141,12 +138,12 @@ class SyncToken:
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")
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:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
- except KeyError:
- books_last_id = -1
+ #except KeyError:
+ # books_last_id = -1
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
@@ -155,7 +152,7 @@ class SyncToken:
archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_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):
@@ -179,16 +176,16 @@ class SyncToken:
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_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)
def __str__(self):
- return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token,
+ return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
self.books_last_created,
self.books_last_modified,
self.archive_last_modified,
self.reading_state_last_modified,
- self.tags_last_modified,
- self.books_last_id)
+ self.tags_last_modified)
+ #self.books_last_id)
diff --git a/cps/services/__init__.py b/cps/services/__init__.py
index e6e5954c..32a9d485 100644
--- a/cps/services/__init__.py
+++ b/cps/services/__init__.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
-
from .. import logger
diff --git a/cps/services/gmail.py b/cps/services/gmail.py
index baada1f8..ff36b308 100644
--- a/cps/services/gmail.py
+++ b/cps/services/gmail.py
@@ -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 .
+
import os.path
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
diff --git a/cps/services/goodreads_support.py b/cps/services/goodreads_support.py
index 9312bc0f..74e6eba9 100644
--- a/cps/services/goodreads_support.py
+++ b/cps/services/goodreads_support.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import time
from functools import reduce
diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py
index 4125bdab..1ca7e5bf 100644
--- a/cps/services/simpleldap.py
+++ b/cps/services/simpleldap.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import base64
from flask_simpleldap import LDAP, LDAPException
diff --git a/cps/services/worker.py b/cps/services/worker.py
index 1baf25fe..5952c705 100644
--- a/cps/services/worker.py
+++ b/cps/services/worker.py
@@ -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 .
-from __future__ import division, print_function, unicode_literals
import threading
import abc
import uuid
@@ -205,10 +221,13 @@ class CalibreTask:
# By default, we're good to clean a task if it's "Done"
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
- @progress.setter
- def progress(self, x):
- # todo: throw error if outside of [0,1]
- self._progress = x
+ '''@progress.setter
+ def progress(self, x):
+ if x > 1:
+ x = 1
+ if x < 0:
+ x = 0
+ self._progress = x'''
def _handleError(self, error_message):
self.stat = STAT_FAIL
diff --git a/cps/shelf.py b/cps/shelf.py
index d232e850..09fe8dd4 100644
--- a/cps/shelf.py
+++ b/cps/shelf.py
@@ -20,8 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
-
import sys
from datetime import datetime
diff --git a/cps/static/js/libs/djvu_html5/Djvu_html5.css b/cps/static/css/libs/Djvu_html5.css
similarity index 93%
rename from cps/static/js/libs/djvu_html5/Djvu_html5.css
rename to cps/static/css/libs/Djvu_html5.css
index 77e37c97..a7ed799e 100644
--- a/cps/static/js/libs/djvu_html5/Djvu_html5.css
+++ b/cps/static/css/libs/Djvu_html5.css
@@ -35,7 +35,7 @@ body {
float: left;
width: 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-size: 500% 300%;
}
@@ -185,10 +185,10 @@ _:-ms-lang(x), .textLayer {
width: 128px;
height: 128px;
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;
}
.blankImage {
- background-image: url("img/blank.jpg");
-}
\ No newline at end of file
+ background-image: url("../../js/libs/djvu_html5/img/blank.jpg");
+}
diff --git a/cps/static/css/libs/typeahead.css b/cps/static/css/libs/typeahead.css
index fde250d6..fcc17a5b 100644
--- a/cps/static/css/libs/typeahead.css
+++ b/cps/static/css/libs/typeahead.css
@@ -145,7 +145,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
cursor: not-allowed;
background-color: #eeeeee !important;
}
-.tt-dropdown-menu {
+.tt-menu {
position: absolute;
top: 100%;
left: 0;
@@ -166,7 +166,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
*border-right-width: 2px;
*border-bottom-width: 2px;
}
-.tt-dropdown-menu .tt-suggestion {
+.tt-menu .tt-suggestion {
display: block;
padding: 3px 20px;
clear: both;
@@ -175,15 +175,15 @@ fieldset[disabled] .twitter-typeahead .tt-input {
color: #333333;
white-space: nowrap;
}
-.tt-dropdown-menu .tt-suggestion.tt-cursor {
+.tt-menu .tt-suggestion.tt-cursor {
text-decoration: none;
outline: 0;
background-color: #f5f5f5;
color: #262626;
}
-.tt-dropdown-menu .tt-suggestion.tt-cursor a {
+.tt-menu .tt-suggestion.tt-cursor a {
color: #262626;
}
-.tt-dropdown-menu .tt-suggestion p {
+.tt-menu .tt-suggestion p {
margin: 0;
}
diff --git a/cps/static/css/style.css b/cps/static/css/style.css
index 5b7715fd..2bbdf370 100644
--- a/cps/static/css/style.css
+++ b/cps/static/css/style.css
@@ -123,6 +123,10 @@ table .bg-dark-danger a { color: #fff; }
flex-wrap: wrap;
}
+.row-fluid.text-center {
+ margin-top: -20px;
+}
+
.container-fluid img {
display: block;
max-width: 100%;
@@ -166,6 +170,10 @@ table .bg-dark-danger a { color: #fff; }
box-shadow: 0 5px 8px -6px #777;
}
+.datepicker.form-control {
+ position: static;
+}
+
.container-fluid .book .cover span img {
position: relative;
top: 0;
@@ -322,7 +330,7 @@ table .bg-dark-danger:hover { background-color: #c9302c; }
table .bg-primary:hover { background-color: #1c5484; }
.block-label { display: block; }
-.fake-input {
+.form-control.fake-input {
position: absolute;
pointer-events: none;
top: 0;
diff --git a/cps/static/css/text.css b/cps/static/css/text.css
new file mode 100644
index 00000000..74204680
--- /dev/null
+++ b/cps/static/css/text.css
@@ -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;
+}
diff --git a/cps/static/js/details.js b/cps/static/js/details.js
index 951e404a..9caf9470 100644
--- a/cps/static/js/details.js
+++ b/cps/static/js/details.js
@@ -22,15 +22,17 @@ $(function() {
});
$("#have_read_cb").on("change", function() {
- $.post({
+ $.ajax({
url: this.closest("form").action,
+ method:"post",
+ data: $(this).closest("form").serialize(),
error: function(response) {
var data = [{type:"danger", message:response.responseText}]
$("#flash_success").remove();
$("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) {
- $(".navbar").after('
');
});
@@ -579,7 +594,7 @@ $(function() {
method:"post",
dataType: "json",
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) {
if ( data.change ) {
if ( data.valid ) {
@@ -652,10 +667,10 @@ $(function() {
var folder = target.data("folderonly");
var filter = target.data("filefilter");
$("#element_selected").text(path);
- $("#file_confirm")[0].attributes["data-link"].value = target.data("link");
- $("#file_confirm")[0].attributes["data-folderonly"].value = (typeof folder === 'undefined') ? false : true;
- $("#file_confirm")[0].attributes["data-filefilter"].value = (typeof filter === 'undefined') ? "" : filter;
- $("#file_confirm")[0].attributes["data-newfile"].value = target.data("newfile");
+ $("#file_confirm").data("link", target.data("link"));
+ $("#file_confirm").data("folderonly", (typeof folder === 'undefined') ? false : true);
+ $("#file_confirm").data("filefilter", (typeof filter === 'undefined') ? "" : filter);
+ $("#file_confirm").data("newfile", target.data("newfile"));
fillFileTable(path,"dir", folder, filter);
});
@@ -669,7 +684,7 @@ $(function() {
var folder = $(file_confirm).data("folderonly");
var filter = $(file_confirm).data("filefilter");
var newfile = $(file_confirm).data("newfile");
- if (newfile !== 'undefined') {
+ if (newfile !== "") {
$("#element_selected").text(path + $("#new_file".text()));
} else {
$("#element_selected").text(path);
@@ -715,7 +730,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../ajax/view",
+ url: getPath() + "/ajax/view",
data: "{\"series\": {\"series_view\": \""+ view +"\"}}",
success: function success() {
location.reload();
diff --git a/cps/static/js/reading/djvu_reader.js b/cps/static/js/reading/djvu_reader.js
new file mode 100644
index 00000000..c93d705a
--- /dev/null
+++ b/cps/static/js/reading/djvu_reader.js
@@ -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 .
+ */
+
+var DJVU_CONTEXT = {
+ background: "#666",
+ uiHideDelay: 1500,
+};
diff --git a/cps/static/js/reading/txt_reader.js b/cps/static/js/reading/txt_reader.js
new file mode 100644
index 00000000..9cfb51f5
--- /dev/null
+++ b/cps/static/js/reading/txt_reader.js
@@ -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 .
+ */
+
+$(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});
+ }
+});
diff --git a/cps/static/js/remote_login.js b/cps/static/js/remote_login.js
new file mode 100644
index 00000000..f0d364dc
--- /dev/null
+++ b/cps/static/js/remote_login.js
@@ -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 .
+ */
+
+(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);
+})()
diff --git a/cps/static/js/shelforder.js b/cps/static/js/shelforder.js
index 62b7e9f3..5cbe7109 100644
--- a/cps/static/js/shelforder.js
+++ b/cps/static/js/shelforder.js
@@ -35,6 +35,7 @@ function sendData(path) {
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", path);
+ // form.setAttribute("csrf_token", );
for (counter = 0;counter < maxElements;counter++) {
tmp[counter] = elements[counter].getAttribute("id");
@@ -44,6 +45,10 @@ function sendData(path) {
hiddenField.setAttribute("value", String(counter + 1));
form.appendChild(hiddenField);
}
+ $("")
+ .attr("name", "csrf_token").val($("input[name='csrf_token']").val())
+ .appendTo(form);
+
document.body.appendChild(form);
form.submit();
}
diff --git a/cps/static/js/table.js b/cps/static/js/table.js
index 9fa28281..a55ec5d1 100644
--- a/cps/static/js/table.js
+++ b/cps/static/js/table.js
@@ -85,7 +85,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../../ajax/mergebooks",
+ url: window.location.pathname + "/../ajax/mergebooks",
data: JSON.stringify({"Merge_books":selections}),
success: function success() {
$("#books-table").bootstrapTable("refresh");
@@ -104,7 +104,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../../ajax/simulatemerge",
+ url: window.location.pathname + "/../ajax/simulatemerge",
data: JSON.stringify({"Merge_books":selections}),
success: function success(booTitles) {
$.each(booTitles.from, function(i, item) {
@@ -121,7 +121,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../../ajax/xchange",
+ url: window.location.pathname + "/../ajax/xchange",
data: JSON.stringify({"xchange":selections}),
success: function success() {
$("#books-table").bootstrapTable("refresh");
@@ -161,10 +161,11 @@ $(function() {
$("#books-table").bootstrapTable({
sidePagination: "server",
+ pageList: "[10, 25, 50, 100]",
queryParams: queryParams,
pagination: true,
paginationLoop: false,
- paginationDetailHAlign: " hidden",
+ paginationDetailHAlign: "right",
paginationHAlign: "left",
idField: "id",
uniqueId: "id",
@@ -187,7 +188,7 @@ $(function() {
$.ajax({
method:"get",
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) {
var key = Object.keys(data)[0];
$("#books-table").bootstrapTable("updateCellByUniqueId", {
@@ -215,7 +216,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../../ajax/table_settings",
+ url: window.location.pathname + "/../ajax/table_settings",
data: "{" + st + "}",
});
},
@@ -769,7 +770,7 @@ function handleListServerResponse (data) {
$("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
data.forEach(function(item) {
- $(".navbar").after('
' +
+ $(".navbar").after('
' +
'
' + item.message + '
' +
'
');
});
diff --git a/cps/subproc_wrapper.py b/cps/subproc_wrapper.py
index 3cc4a070..187b2cb2 100644
--- a/cps/subproc_wrapper.py
+++ b/cps/subproc_wrapper.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from __future__ import division, print_function, unicode_literals
import sys
import os
import subprocess
@@ -33,13 +32,8 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro
if key in quotes:
command[key] = '"' + element + '"'
exc_command = " ".join(command)
- if sys.version_info < (3, 0):
- exc_command = exc_command.encode(sys.getfilesystemencoding())
else:
- if sys.version_info < (3, 0):
- exc_command = [x.encode(sys.getfilesystemencoding()) for x in command]
- else:
- exc_command = [x for x in command]
+ exc_command = [x for x in command]
return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec
diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py
index 56cc7076..08fb1644 100644
--- a/cps/tasks/convert.py
+++ b/cps/tasks/convert.py
@@ -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 .
+
import sys
import os
import re
@@ -161,8 +178,6 @@ class TaskConvert(CalibreTask):
while True:
nextline = p.stdout.readlines()
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:
log.debug(line)
if p.poll() is not None:
@@ -207,10 +222,6 @@ class TaskConvert(CalibreTask):
while p.poll() is None:
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'))
# parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline)
@@ -224,8 +235,6 @@ class TaskConvert(CalibreTask):
calibre_traceback = p.stderr.readlines()
error_message = ""
for ele in calibre_traceback:
- if sys.version_info < (3, 0):
- ele = ele.decode('utf-8')
log.debug(ele.strip('\n'))
if not ele.startswith('Traceback') and not ele.startswith(' File'):
error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n'))
diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py
index 292114d5..2e95ee98 100644
--- a/cps/tasks/mail.py
+++ b/cps/tasks/mail.py
@@ -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 .
+
import sys
import os
import smtplib
import threading
import socket
import mimetypes
-import base64
try:
from StringIO import StringIO
@@ -162,17 +178,11 @@ class TaskEmail(CalibreTask):
log.debug_or_exception(ex)
self._handleError(u'Error sending e-mail: {}'.format(ex))
-
def send_standard_email(self, msg):
use_ssl = int(self.settings.get('mail_use_ssl', 0))
timeout = 600 # set timeout to 5mins
- # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten
- # _print_debug function
- if sys.version_info < (3, 0):
- org_smtpstderr = smtplib.stderr
- smtplib.stderr = logger.StderrLogger('worker.smtp')
-
+ # on python3 debugoutput is caught with overwritten _print_debug function
log.debug("Start sending e-mail")
if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
@@ -198,9 +208,6 @@ class TaskEmail(CalibreTask):
self._handleSuccess()
log.debug("E-mail send successfully")
- if sys.version_info < (3, 0):
- smtplib.stderr = org_smtpstderr
-
def send_gmail_email(self, message):
return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message)
@@ -218,7 +225,6 @@ class TaskEmail(CalibreTask):
self.asyncSMTP = None
self._progress = x
-
@classmethod
def _get_attachment(cls, bookpath, filename):
"""Get file as MIMEBase message"""
diff --git a/cps/tasks/upload.py b/cps/tasks/upload.py
index d7ef34c2..6a341cdd 100644
--- a/cps/tasks/upload.py
+++ b/cps/tasks/upload.py
@@ -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 .
from datetime import datetime
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
diff --git a/cps/templates/admin.html b/cps/templates/admin.html
index 81ef955b..9a941594 100644
--- a/cps/templates/admin.html
+++ b/cps/templates/admin.html
@@ -186,6 +186,7 @@
{% if feature_support['updater'] %}
+