Merge branch 'master' into Develop

This commit is contained in:
Ozzie Isaacs 2022-06-04 12:05:34 +02:00
commit 909797dc49
73 changed files with 19460 additions and 16482 deletions

11
README.md Normal file → Executable file
View File

@ -21,13 +21,14 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- Admin interface
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
- OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language
- Filter and search by titles, authors, tags, series, book format and language
- Create a custom book collection (shelves)
- Support for editing eBook metadata and deleting eBooks from Calibre library
- Support for downloading eBook metadata from various sources, sources can be extended via external plugins
- Support for converting eBooks through Calibre binaries
- Restrict eBook download to logged-in users
- Support for public user registration
- Send eBooks to Kindle devices with the click of a button
- Send eBooks to E-Readers with the click of a button
- Sync your Kobo devices through Calibre-Web with your Calibre library
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
@ -42,7 +43,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
#### Installation via pip (recommended)
1. To avoid problems with already installed python dependencies, it's recommended to create a virtual environment for Calibre-Web
2. Install Calibre-Web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
4. Calibre-Web can be started afterwards by typing `cps`
In the Wiki there are also examples for: a [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [installation on Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20), [installation on a Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
@ -52,7 +53,7 @@ In the Wiki there are also examples for: a [manual installation](https://github.
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
Login with default admin login \
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button \
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) \
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration) \
Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page)
#### Default admin login:
@ -64,7 +65,7 @@ Afterwards you can configure your Calibre-Web instance ([Basic Configuration](ht
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-ereader feature, or during editing of ebooks metadata:
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.

View File

@ -35,7 +35,7 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) ||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|

View File

@ -25,7 +25,6 @@ import platform
import sqlite3
from collections import OrderedDict
import werkzeug
import flask
import flask_login
import jinja2
@ -37,41 +36,40 @@ from .render_template import render_title_template
about = flask.Blueprint('about', __name__)
ret = dict()
req = dep_check.load_dependencys(False)
opt = dep_check.load_dependencys(True)
modules = dict()
req = dep_check.load_dependencies(False)
opt = dep_check.load_dependencies(True)
for i in (req + opt):
ret[i[1]] = i[0]
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
calibre_web_version = constants.STABLE_VERSION['version']
else:
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%', '%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%', '%%'))
if getattr(sys, 'frozen', False):
calibre_web_version += " - Exe-Version"
elif constants.HOME_CONFIG:
calibre_web_version += " - pyPi"
_VERSIONS = OrderedDict(
Platform='{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version,
Calibre_Web=calibre_web_version,
Werkzeug=werkzeug.__version__,
Jinja2=jinja2.__version__,
pySqlite=sqlite3.version,
SQLite=sqlite3.sqlite_version,
)
_VERSIONS.update(ret)
_VERSIONS.update(uploader.get_versions())
modules[i[1]] = i[0]
modules['Jinja2'] = jinja2.__version__
modules['pySqlite'] = sqlite3.version
modules['SQLite'] = sqlite3.sqlite_version
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
def collect_stats():
_VERSIONS['ebook converter'] = converter.get_calibre_version()
_VERSIONS['unrar'] = converter.get_unrar_version()
_VERSIONS['kepubify'] = converter.get_kepubify_version()
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
calibre_web_version = constants.STABLE_VERSION['version']
else:
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%', '%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%', '%%'))
if getattr(sys, 'frozen', False):
calibre_web_version += " - Exe-Version"
elif constants.HOME_CONFIG:
calibre_web_version += " - pyPi"
_VERSIONS = {'Calibre Web': calibre_web_version}
_VERSIONS.update(OrderedDict(
Python=sys.version,
Platform='{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
))
_VERSIONS.update(uploader.get_magick_version())
_VERSIONS['Unrar'] = converter.get_unrar_version()
_VERSIONS['Ebook converter'] = converter.get_calibre_version()
_VERSIONS['Kepubify'] = converter.get_kepubify_version()
_VERSIONS.update(sorted_modules)
return _VERSIONS
@ -80,7 +78,7 @@ def collect_stats():
def stats():
counter = calibre_db.session.query(db.Books).count()
authors = calibre_db.session.query(db.Authors).count()
categorys = calibre_db.session.query(db.Tags).count()
categories = calibre_db.session.query(db.Tags).count()
series = calibre_db.session.query(db.Series).count()
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
categorycounter=categories, seriecounter=series, title=_(u"Statistics"), page="stat")

28
cps/admin.py Normal file → Executable file
View File

@ -25,7 +25,9 @@ import re
import base64
import json
import operator
from datetime import datetime, timedelta, time
import time
from datetime import datetime, timedelta
from datetime import time as datetime_time
from functools import wraps
@ -158,7 +160,7 @@ def shutdown():
return json.dumps(showtext), 400
# method is available without login and not protected by CSRF to make it easy reachable, is per default switched of
# method is available without login and not protected by CSRF to make it easy reachable, is per default switched off
# needed for docker applications, as changes on metadata.db from host are not visible to application
@admi.route("/reconnect", methods=['GET'])
def reconnect():
@ -205,7 +207,7 @@ def admin():
all_user = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings()
schedule_time = format_time(time(hour=config.schedule_start_time), format="short")
schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short")
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
schedule_duration = format_timedelta(t, threshold=.99)
@ -613,7 +615,8 @@ def load_dialogtexts(element_id):
elif element_id == "db_submit":
texts["main"] = _('Are you sure you want to change Calibre library location?')
elif element_id == "admin_refresh_cover_cache":
texts["main"] = _('Calibre-Web will search for updated Covers and update Cover Thumbnails, this may take a while?')
texts["main"] = _('Calibre-Web will search for updated Covers '
'and update Cover Thumbnails, this may take a while?')
elif element_id == "btnfullsync":
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database "
"to force a full sync with your Kobo Reader?")
@ -744,6 +747,7 @@ def edit_restriction(res_type, user_id):
ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value))
return ""
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
@ -1082,7 +1086,7 @@ def _configuration_gdrive_helper(to_save):
gdrive_secrets['redirect_uris'][0]
)
# always show google drive settings, but in case of error deny support
# always show Google Drive settings, but in case of error deny support
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
if config.config_use_google_drive and not new_gdrive_value:
config.config_google_drive_watch_changes_response = {}
@ -1300,7 +1304,7 @@ def edit_scheduledtasks():
duration_field = list()
for n in range(24):
time_field.append((n , format_time(time(hour=n), format="short",)))
time_field.append((n, format_time(datetime_time(hour=n), format="short",)))
for n in range(5, 65, 5):
t = timedelta(hours=n // 60, minutes=n % 60)
duration_field.append((n, format_timedelta(t, threshold=.9)))
@ -1519,11 +1523,11 @@ def ldap_import_create_user(user, user_data):
log.warning("LDAP User %s Already in Database", user_data)
return 0, None
kindlemail = ''
ereader_mail = ''
if 'mail' in user_data:
useremail = user_data['mail'][0].decode('utf-8')
if len(user_data['mail']) > 1:
kindlemail = user_data['mail'][1].decode('utf-8')
ereader_mail = user_data['mail'][1].decode('utf-8')
else:
log.debug('No Mail Field Found in LDAP Response')
@ -1539,7 +1543,7 @@ def ldap_import_create_user(user, user_data):
content.name = username
content.password = '' # dummy password which will be replaced by ldap one
content.email = useremail
content.kindle_mail = kindlemail
content.kindle_mail = ereader_mail
content.default_language = config.config_default_language
content.locale = config.config_default_locale
content.role = config.config_default_role
@ -1835,7 +1839,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
log.info("Missing entries on new user")
raise Exception(_(u"Please fill out all fields!"))
content.email = check_email(to_save["email"])
# Query User name, if not existing, change
# Query username, if not existing, change
content.name = check_username(to_save["name"])
if to_save.get("kindle_mail"):
content.kindle_mail = valid_email(to_save["kindle_mail"])
@ -1954,7 +1958,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
try:
if to_save.get("email", content.email) != content.email:
content.email = check_email(to_save["email"])
# Query User name, if not existing, change
# Query username, if not existing, change
if to_save.get("name", content.name) != content.name:
if to_save.get("name") == "Guest":
raise Exception(_("Guest Name can't be changed"))
@ -1990,7 +1994,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
def extract_user_data_from_field(user, field):
match = re.search(field + r"=([\.\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
match = re.search(field + r"=([@\.\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
if match:
return match.group(1)
else:

View File

@ -32,8 +32,10 @@ def get_locale():
def get_user_locale_language(user_language):
return Locale.parse(user_language).get_language_name(get_locale())
def get_available_locale():
return [Locale('en')] + babel.list_translations()
def get_available_translations():
return set(str(item) for item in get_available_locale())

View File

@ -49,7 +49,7 @@ def init_cache_busting(app):
rooted_filename = os.path.join(dirpath, filename)
try:
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
@ -59,11 +59,11 @@ def init_cache_busting(app):
log.debug('Finished computing cache-busting values')
def bust_filename(filename):
return hash_table.get(filename, "")
def bust_filename(file_name):
return hash_table.get(file_name, "")
def unbust_filename(filename):
return filename.split("?", 1)[0]
def unbust_filename(file_name):
return file_name.split("?", 1)[0]
@app.url_defaults
# pylint: disable=unused-variable

View File

@ -26,26 +26,28 @@ from .constants import STABLE_VERSION as _STABLE_VERSION
from .constants import NIGHTLY_VERSION as _NIGHTLY_VERSION
from .constants import DEFAULT_SETTINGS_FILE, DEFAULT_GDRIVE_FILE
def version_info():
if _NIGHTLY_VERSION[1].startswith('$Format'):
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
class CliParameter(object):
def init(self):
self.arg_parser()
def arg_parser(self):
parser = argparse.ArgumentParser(description='Calibre Web is a web app'
' providing a interface for browsing, reading and downloading eBooks\n',
parser = argparse.ArgumentParser(description='Calibre Web is a web app providing '
'a interface for browsing, reading and downloading eBooks\n',
prog='cps.py')
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
parser.add_argument('-c', metavar='path',
help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile')
parser.add_argument('-k', metavar='path',
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
parser.add_argument('-c', metavar='path', help='path and name to SSL certfile, e.g. /opt/test.cert, '
'works only in combination with keyfile')
parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, '
'works only in combination with certfile')
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
@ -67,7 +69,6 @@ class CliParameter(object):
if os.path.isdir(self.gd_path):
self.gd_path = os.path.join(self.gd_path, DEFAULT_GDRIVE_FILE)
# handle and check parameter for ssl encryption
self.certfilepath = None
self.keyfilepath = None
@ -96,7 +97,7 @@ class CliParameter(object):
self.keyfilepath = ""
# dry run updater
self.dry_run =args.d or None
self.dry_run = args.d or None
# enable reconnect endpoint for docker database reconnect
self.reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
# load covers from localhost
@ -112,7 +113,7 @@ class CliParameter(object):
else:
socket.inet_pton(socket.AF_INET, self.ip_address)
else:
# on windows python < 3.4, inet_pton is not available
# on Windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(self.ip_address)
except socket.error as err:

View File

@ -35,6 +35,7 @@ from . import constants, logger
log = logger.create()
_Base = declarative_base()
class _Flask_Settings(_Base):
__tablename__ = 'flask_settings'
@ -67,14 +68,14 @@ class _Settings(_Base):
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
config_certfile = Column(String)
config_keyfile = Column(String)
config_trustedhosts = Column(String,default='')
config_trustedhosts = Column(String, default='')
config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4)
config_authors_max = Column(Integer, default=0)
config_read_column = Column(Integer, default=0)
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
config_mature_content_tags = Column(String, default='')
# config_mature_content_tags = Column(String, default='')
config_theme = Column(Integer, default=0)
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
@ -123,7 +124,7 @@ class _Settings(_Base):
config_ldap_key_path = Column(String, default="")
config_ldap_dn = Column(String, default='dc=example,dc=org')
config_ldap_user_object = Column(String, default='uid=%s')
config_ldap_member_user_object = Column(String, default='') #
config_ldap_member_user_object = Column(String, default='')
config_ldap_openldap = Column(Boolean, default=True)
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
config_ldap_group_members_field = Column(String, default='memberUid')
@ -171,7 +172,6 @@ class _ConfigSQL(object):
self.config_converterpath = autodetect_calibre_binary()
if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
change = True
self.config_kepubifypath = autodetect_kepubify_binary()
@ -257,14 +257,14 @@ class _ConfigSQL(object):
return logger.get_level_name(self.config_log_level)
def get_mail_settings(self):
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
return {k: v for k, v in self.__dict__.items() if k.startswith('mail_')}
def get_mail_server_configured(self):
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
def get_scheduled_task_settings(self):
return {k:v for k, v in self.__dict__.items() if k.startswith('schedule_')}
return {k: v for k, v in self.__dict__.items() if k.startswith('schedule_')}
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
"""Possibly updates a field of this object.
@ -301,7 +301,7 @@ class _ConfigSQL(object):
return storage
def load(self):
'''Load all configuration values from the underlying storage.'''
"""Load all configuration values from the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in s.__dict__.items():
if k[0] != '_':
@ -334,7 +334,7 @@ class _ConfigSQL(object):
self._session.rollback()
def save(self):
'''Apply all configuration values to the underlying storage.'''
"""Apply all configuration values to the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in self.__dict__.items():
@ -369,6 +369,7 @@ class _ConfigSQL(object):
except AttributeError:
pass
def _migrate_table(session, orm_class):
changed = False
@ -390,9 +391,9 @@ def _migrate_table(session, orm_class):
else:
column_type = column.type
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column_type,
column_default))
column_name,
column_type,
column_default))
log.debug(alter_table)
session.execute(alter_table)
changed = True
@ -462,6 +463,7 @@ def load_configuration(conf, session, cli):
conf.init_config(session, cli)
# return conf
def get_flask_session_key(_session):
flask_settings = _session.query(_Flask_Settings).one_or_none()
if flask_settings == None:

View File

@ -54,10 +54,9 @@ def get_calibre_version():
def get_unrar_version():
unrar_version = _get_command_version(config.config_rarfile_location, r'UNRAR.*\d')
if unrar_version == "not installed":
unrar_version = _get_command_version(config.config_rarfile_location, r'unrar.*\d','-V')
unrar_version = _get_command_version(config.config_rarfile_location, r'unrar.*\d', '-V')
return unrar_version
def get_kepubify_version():
return _get_command_version(config.config_kepubifypath, r'kepubify\s','--version')
return _get_command_version(config.config_kepubifypath, r'kepubify\s', '--version')

View File

@ -19,7 +19,6 @@
import os
import re
import ast
import json
from datetime import datetime
from urllib.parse import quote
@ -388,7 +387,7 @@ class CustomColumns(Base):
normalized = Column(Boolean)
def get_display_dict(self):
display_dict = ast.literal_eval(self.display)
display_dict = json.loads(self.display)
return display_dict

View File

@ -32,6 +32,7 @@ from .about import collect_stats
log = logger.create()
def assemble_logfiles(file_name):
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
wfd = BytesIO()

View File

@ -20,7 +20,8 @@ if not importlib:
except ImportError as e:
pkgresources = False
def load_dependencys(optional=False):
def load_dependencies(optional=False):
deps = list()
if getattr(sys, 'frozen', False):
pip_installed = os.path.join(BASE_DIR, ".pip_installed")
@ -41,7 +42,7 @@ def load_dependencys(optional=False):
res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip())
try:
if getattr(sys, 'frozen', False):
dep_version = exe_deps[res.group(1).lower().replace('_','-')]
dep_version = exe_deps[res.group(1).lower().replace('_', '-')]
else:
if importlib:
dep_version = version(res.group(1))
@ -57,38 +58,38 @@ def load_dependencys(optional=False):
def dependency_check(optional=False):
d = list()
deps = load_dependencys(optional)
deps = load_dependencies(optional)
for dep in deps:
try:
dep_version_int = [int(x) for x in dep[0].split('.')]
low_check = [int(x) for x in dep[3].split('.')]
high_check = [int(x) for x in dep[5].split('.')]
except AttributeError:
high_check = None
high_check = []
except ValueError:
d.append({'name': dep[1],
'target': "available",
'found': "Not available"
})
'target': "available",
'found': "Not available"
})
continue
if dep[2].strip() == "==":
if dep_version_int != low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
'found': dep[0],
"target": dep[2] + dep[3]})
continue
elif dep[2].strip() == ">=":
if dep_version_int < low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
'found': dep[0],
"target": dep[2] + dep[3]})
continue
elif dep[2].strip() == ">":
if dep_version_int <= low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
'found': dep[0],
"target": dep[2] + dep[3]})
continue
if dep[4] and dep[5]:
if dep[4].strip() == "<":

71
cps/helper.py Normal file → Executable file
View File

@ -72,7 +72,7 @@ except (ImportError, RuntimeError) as e:
# Convert existing book entry to new format
def convert_book_format(book_id, calibre_path, old_book_format, new_book_format, user_id, kindle_mail=None):
def convert_book_format(book_id, calibre_path, old_book_format, new_book_format, user_id, ereader_mail=None):
book = calibre_db.get_book(book_id)
data = calibre_db.get_book_format(book.id, old_book_format)
file_path = os.path.join(calibre_path, book.path, data.name)
@ -91,9 +91,9 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
# read settings and append converter task to queue
if kindle_mail:
if ereader_mail:
settings = config.get_mail_settings()
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['subject'] = _('Send to E-Reader') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
else:
settings = dict()
@ -104,14 +104,14 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
link)
settings['old_book_format'] = old_book_format
settings['new_book_format'] = new_book_format
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, ereader_mail, user_id))
return None
# Texts are not lazy translated as they are supposed to get send out as is
def send_test_mail(kindle_mail, user_name):
def send_test_mail(ereader_mail, user_name):
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
config.get_mail_settings(), kindle_mail, N_(u"Test e-mail"),
config.get_mail_settings(), ereader_mail, N_(u"Test e-mail"),
_(u'This e-mail has been sent via Calibre-Web.')))
return
@ -139,26 +139,26 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
return
def check_send_to_kindle_with_converter(formats):
def check_send_to_ereader_with_converter(formats):
book_formats = list()
if 'EPUB' in formats and 'MOBI' not in formats:
book_formats.append({'format': 'Mobi',
if 'MOBI' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub',
'convert': 1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and 'MOBI' not in formats:
book_formats.append({'format': 'Mobi',
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
orig='Mobi',
format='Epub')})
if 'AZW3' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
orig='Azw3',
format='Mobi')})
format='Epub')})
return book_formats
def check_send_to_kindle(entry):
def check_send_to_ereader(entry):
"""
returns all available book formats for sending to Kindle
returns all available book formats for sending to E-Reader
"""
formats = list()
book_formats = list()
@ -166,20 +166,24 @@ def check_send_to_kindle(entry):
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'EPUB' in formats:
book_formats.append({'format': 'Epub',
'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Epub')})
if 'MOBI' in formats:
book_formats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
'text': _('Send %(format)s to E-Reader', format='Mobi')})
if 'PDF' in formats:
book_formats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
'text': _('Send %(format)s to E-Reader', format='Pdf')})
if 'AZW' in formats:
book_formats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
'text': _('Send %(format)s to E-Reader', format='Azw')})
if config.config_converterpath:
book_formats.extend(check_send_to_kindle_with_converter(formats))
book_formats.extend(check_send_to_ereader_with_converter(formats))
return book_formats
else:
log.error(u'Cannot find book entry %d', entry.id)
@ -199,27 +203,27 @@ def check_read_formats(entry):
# Files are processed in the following order/priority:
# 1: If Mobi file is existing, it's directly send to kindle email,
# 2: If Epub file is existing, it's converted and send to kindle email,
# 3: If Pdf file is existing, it's directly send to kindle email
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
# 1: If Mobi file is existing, it's directly send to E-Reader email,
# 2: If Epub file is existing, it's converted and send to E-Reader email,
# 3: If Pdf file is existing, it's directly send to E-Reader email
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
"""Send email with attachments"""
book = calibre_db.get_book(book_id)
if convert == 1:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, kindle_mail)
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, ereader_mail)
if convert == 2:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, kindle_mail)
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, ereader_mail)
for entry in iter(book.data):
if entry.format.upper() == book_format.upper():
converted_file_name = entry.name + '.' + book_format.lower()
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
email_text = N_(u"%(book)s send to Kindle", book=link)
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
config.get_mail_settings(), kindle_mail,
email_text = N_(u"%(book)s send to E-Reader", book=link)
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name,
config.get_mail_settings(), ereader_mail,
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
return
return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -241,8 +245,7 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
# pipe has to be replaced with comma
value = re.sub(r'[|]+', u',', value, flags=re.U)
filename_encoding_for_length = 'utf-16' if sys.platform == "win32" or sys.platform == "darwin" else 'utf-8'
value = value.encode(filename_encoding_for_length)[:chars].decode('utf-8', errors='ignore').strip()
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
if not value:
raise ValueError("Filename cannot be empty")
@ -805,7 +808,7 @@ def save_cover_from_url(url, book_path):
img = advocate.get(url, timeout=(10, 200), allow_redirects=False) # ToDo: Error Handling
else:
log.error("python module advocate is not installed but is needed")
return False, _("Python module 'advocate' is not installed but is needed for cover downloads")
return False, _("Python module 'advocate' is not installed but is needed for cover uploads")
img.raise_for_status()
return save_cover(img, book_path)
except (socket.gaierror,

View File

@ -54,7 +54,6 @@ class _Logger(logging.Logger):
else:
self.error(message, *args, **kwargs)
def debug_no_auth(self, message, *args, **kwargs):
message = message.strip("\r\n")
if message.startswith("send: AUTH"):
@ -66,6 +65,7 @@ class _Logger(logging.Logger):
def get(name=None):
return logging.getLogger(name)
def create():
parent_frame = inspect.stack(0)[1]
if hasattr(parent_frame, 'frame'):
@ -75,9 +75,11 @@ def create():
parent_module = inspect.getmodule(parent_frame)
return get(parent_module.__name__)
def is_debug_enabled():
return logging.root.level <= logging.DEBUG
def is_info_enabled(logger):
return logging.getLogger(logger).level <= logging.INFO
@ -114,10 +116,10 @@ def get_accesslogfile(log_file):
def setup(log_file, log_level=None):
'''
"""
Configure the logging output.
May be called multiple times.
'''
"""
log_level = log_level or DEFAULT_LOG_LEVEL
logging.setLoggerClass(_Logger)
logging.getLogger(__package__).setLevel(log_level)
@ -127,7 +129,7 @@ def setup(log_file, log_level=None):
# avoid spamming the log with debug messages from libraries
r.setLevel(log_level)
# Otherwise name get's destroyed on windows
# Otherwise, name gets destroyed on Windows
if log_file != LOG_TO_STDERR and log_file != LOG_TO_STDOUT:
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
@ -159,13 +161,14 @@ def setup(log_file, log_level=None):
r.removeHandler(h)
h.close()
r.addHandler(file_handler)
logging.captureWarnings(True)
return "" if log_file == DEFAULT_LOG_FILE else log_file
def create_access_log(log_file, log_name, formatter):
'''
"""
One-time configuration for the web server's access log.
'''
"""
log_file = _absolute_log_file(log_file, DEFAULT_ACCESS_LOG)
logging.debug("access log: %s", log_file)
@ -182,8 +185,7 @@ def create_access_log(log_file, log_name, formatter):
file_handler.setFormatter(formatter)
access_log.addHandler(file_handler)
return access_log, \
"" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
return access_log, "" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
# Enable logging of smtp lib debug output

View File

@ -25,7 +25,7 @@
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# http://flask.pocoo.org/snippets/62/
# https://web.archive.org/web/20120517003641/http://flask.pocoo.org/snippets/62/
from urllib.parse import urlparse, urljoin

View File

@ -204,6 +204,7 @@ class WebServer(object):
output = _readable_listen_address(self.listen_address, self.listen_port)
log.info('Starting Gevent server on %s', output)
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
error_log=log,
spawn=Pool(), **ssl_args)
if ssl_args:
wrap_socket = self.wsgiserver.wrap_socket

16
cps/static/js/caliBlur.js Normal file → Executable file
View File

@ -201,7 +201,7 @@ if ($("body.book").length > 0) {
// Move dropdown lists higher in dom, replace bootstrap toggle with own toggle.
$('ul[aria-labelledby="read-in-browser"]').insertBefore(".blur-wrapper").addClass("readinbrowser-drop");
$('ul[aria-labelledby="send-to-kindle"]').insertBefore(".blur-wrapper").addClass("sendtokindle-drop");
$('ul[aria-labelledby="send-to-kereader"]').insertBefore(".blur-wrapper").addClass("sendtoereader-drop");
$(".leramslist").insertBefore(".blur-wrapper");
$('ul[aria-labelledby="btnGroupDrop1"]').insertBefore(".blur-wrapper").addClass("leramslist");
$("#add-to-shelves").insertBefore(".blur-wrapper");
@ -215,7 +215,7 @@ if ($("body.book").length > 0) {
});
$("#sendbtn2").click(function () {
$(".sendtokindle-drop").toggle();
$(".sendtoereader-drop").toggle();
});
@ -242,12 +242,12 @@ if ($("body.book").length > 0) {
if ($("#sendbtn2").length > 0) {
position = $("#sendbtn2").offset().left
if (position + $(".sendtokindle-drop").width() > $(window).width()) {
positionOff = position + $(".sendtokindle-drop").width() - $(window).width();
if (position + $(".sendtoereader-drop").width() > $(window).width()) {
positionOff = position + $(".sendtoereader-drop").width() - $(window).width();
ribPosition = position - positionOff - 5
$(".sendtokindle-drop").attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
$(".sendtoereader-drop").attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
} else {
$(".sendtokindle-drop").attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
$(".sendtoereader-drop").attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
}
}
@ -300,7 +300,7 @@ if ($("body.book").length > 0) {
$(document).mouseup(function (e) {
var container = new Array();
container.push($('ul[aria-labelledby="read-in-browser"]'));
container.push($(".sendtokindle-drop"));
container.push($(".sendtoereader-drop"));
container.push($(".leramslist"));
container.push($("#add-to-shelves"));
container.push($(".navbar-collapse.collapse.in"));
@ -666,7 +666,7 @@ $("#sendbtn").attr({
$("#sendbtn2").attr({
"data-toggle-two": "tooltip",
"title": $("#sendbtn2").text(), // "Send to Kindle",
"title": $("#sendbtn2").text(), // "Send to E-Reader",
"data-placement": "bottom",
"data-viewport": ".btn-toolbar"
})

0
cps/static/js/main.js Executable file → Normal file
View File

16
cps/tasks/convert.py Normal file → Executable file
View File

@ -41,13 +41,13 @@ log = logger.create()
class TaskConvert(CalibreTask):
def __init__(self, file_path, book_id, task_message, settings, kindle_mail, user=None):
def __init__(self, file_path, book_id, task_message, settings, ereader_mail, user=None):
super(TaskConvert, self).__init__(task_message)
self.file_path = file_path
self.book_id = book_id
self.title = ""
self.settings = settings
self.kindle_mail = kindle_mail
self.ereader_mail = ereader_mail
self.user = user
self.results = dict()
@ -85,16 +85,16 @@ class TaskConvert(CalibreTask):
# Upload files to gdrive
gdriveutils.updateGdriveCalibreFromLocal()
self._handleSuccess()
if self.kindle_mail:
# if we're sending to kindle after converting, create a one-off task and run it immediately
if self.ereader_mail:
# if we're sending to E-Reader after converting, create a one-off task and run it immediately
# todo: figure out how to incorporate this into the progress
try:
EmailText = N_(u"%(book)s send to Kindle", book=escape(self.title))
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
self.results["path"],
filename,
self.settings,
self.kindle_mail,
self.ereader_mail,
EmailText,
self.settings['body'],
internal=True)
@ -112,7 +112,7 @@ class TaskConvert(CalibreTask):
# check to see if destination format already exists - or if book is in database
# if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work
# this will allow send to E-Reader workflow to continue to work
if os.path.isfile(file_path + format_new_ext) or\
local_db.get_book_format(self.book_id, self.settings['new_book_format']):
log.info("Book id %d already converted to %s", book_id, format_new_ext)
@ -273,7 +273,7 @@ class TaskConvert(CalibreTask):
return N_("Convert")
def __str__(self):
return "Convert {} {}".format(self.book_id, self.kindle_mail)
return "Convert {} {}".format(self.book_id, self.ereader_mail)
@property
def is_cancellable(self):

2
cps/templates/admin.html Normal file → Executable file
View File

@ -12,7 +12,7 @@
<tr>
<th>{{_('Username')}}</th>
<th>{{_('E-mail Address')}}</th>
<th>{{_('Send to Kindle E-mail Address')}}</th>
<th>{{_('Send to E-Reader E-mail Address')}}</th>
<th>{{_('Downloads')}}</th>
<th class="hidden-xs ">{{_('Admin')}}</th>
<th class="hidden-xs hidden-sm">{{_('Password')}}</th>

16
cps/templates/detail.html Normal file → Executable file
View File

@ -10,7 +10,7 @@
</div>
<div class="col-sm-9 col-lg-9 book-meta">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
<div class="btn-group" role="group" aria-label="Download, send to E-Reader, reading">
{% if g.user.role_download() %}
{% if entry.data|length %}
<div class="btn-group" role="group">
@ -37,18 +37,18 @@
</div>
{% endif %}
{% endif %}
{% if g.user.kindle_mail and entry.kindle_list %}
{% if entry.kindle_list.__len__() == 1 %}
<div id="sendbtn" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=entry.kindle_list[0]['format'], convert=entry.kindle_list[0]['convert'])}}" data-text="{{_('Send to Kindle')}}" class="btn btn-primary postAction" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.kindle_list[0]['text']}}</div>
{% if g.user.kindle_mail and entry.email_share_list %}
{% if entry.email_share_list.__len__() == 1 %}
<div id="sendbtn" data-action="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}" data-text="{{_('Send to E-Reader')}}" class="btn btn-primary postAction" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}}</div>
{% else %}
<div class="btn-group" role="group">
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-send"></span>{{_('Send to Kindle')}}
<span class="glyphicon glyphicon-send"></span>{{_('Send to E-Reader')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="send-to-kindle">
{% for format in entry.kindle_list %}
<li><a class="postAction" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
<ul class="dropdown-menu" aria-labelledby="send-to-ereader">
{% for format in entry.email_share_list %}
<li><a class="postAction" data-action="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
{%endfor%}
</ul>
</div>

View File

@ -18,7 +18,7 @@
<div class="btn btn-primary char">{{char.char}}</div>
{% endfor %}
</div>
<div class="update-view btn btn-primary" data-target="series_view" id="list-button" data-view="list">List</div>
<div class="update-view btn btn-primary" data-target="series_view" id="list-button" data-view="list">{{_('List')}}</div>
</div>
{% if entries[0] %}

View File

@ -19,7 +19,7 @@
</div>
{% if data == "series" %}
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">Grid</button>
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">{{_('Grid')}}</button>
{% endif %}
</div>
<div class="container">

2
cps/templates/shelfdown.html Normal file → Executable file
View File

@ -54,7 +54,7 @@
{% endif %}
</div>
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
<div class="btn-group" role="group" aria-label="Download, send to E-Reader, reading">
{% if g.user.role_download() %}
{% if entry.Books.data|length %}
<div class="btn-group" role="group">

View File

@ -30,7 +30,7 @@
<table id="libs" class="table">
<thead>
<tr>
<th>{{_('Program Library')}}</th>
<th>{{_('Program')}}</th>
<th>{{_('Installed Version')}}</th>
</tr>
</thead>

2
cps/templates/user_edit.html Normal file → Executable file
View File

@ -25,7 +25,7 @@
</div>
{% endif %}
<div class="form-group">
<label for="kindle_mail">{{_('Send to Kindle E-mail Address')}}</label>
<label for="kindle_mail">{{_('Send to E-Reader E-mail Address')}}</label>
<input type="email" class="form-control" name="kindle_mail" id="kindle_mail" value="{{ content.kindle_mail if content.kindle_mail != None }}">
</div>
{% if not content.role_anonymous() %}

2
cps/templates/user_table.html Normal file → Executable file
View File

@ -133,7 +133,7 @@
<th data-name="id" data-field="id" id="id" data-visible="false" data-switchable="false"></th>
{{ user_table_row('name', _('Enter Username'), _('Username'), true) }}
{{ user_table_row('email', _('Enter E-mail Address'), _('E-mail Address'), true) }}
{{ user_table_row('kindle_mail', _('Enter Kindle E-mail Address'), _('Kindle E-mail'), false) }}
{{ user_table_row('kindle_mail', _('Enter E-Reader E-mail Address'), _('E-Reader E-mail'), false) }}
{{ user_select_translations('locale', url_for('admin.table_get_locale'), _('Locale'), true) }}
{{ user_select_languages('default_language', url_for('admin.table_get_default_lang'), _('Visible Book Languages'), true) }}
{{ user_table_row('allowed_tags', _("Edit Allowed Tags"), _("Allowed Tags"), false, tags) }}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -231,7 +231,7 @@ def pdf_preview(tmp_file_path, tmp_dir):
return None
def get_versions():
def get_magick_version():
ret = dict()
if not use_generic_pdf_cover:
ret['Image Magick'] = ImageVersion.MAGICK_VERSION

6
cps/web.py Normal file → Executable file
View File

@ -45,7 +45,7 @@ from .search import render_search_results, render_adv_search_results
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, check_email, check_username, \
get_book_cover, get_series_cover_thumbnail, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email, \
send_registration_mail, check_send_to_ereader, check_read_formats, tags_filters, reset_password, valid_email, \
edit_book_read_status
from .pagination import Pagination
from .redirect import redirect_back
@ -1189,7 +1189,7 @@ def download_link(book_id, book_format, anyname):
@web.route('/send/<int:book_id>/<book_format>/<int:convert>', methods=["POST"])
@login_required
@download_required
def send_to_kindle(book_id, book_format, convert):
def send_to_ereader(book_id, book_format, convert):
if not config.get_mail_server_configured():
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
elif current_user.kindle_mail:
@ -1521,7 +1521,7 @@ def show_book(book_id):
entry.ordered_authors = calibre_db.order_authors([entry])
entry.kindle_list = check_send_to_kindle(entry)
entry.email_share_list = check_send_to_ereader(entry)
entry.reader_list = check_read_formats(entry)
entry.audio_entries = []

File diff suppressed because it is too large Load Diff

View File

@ -41,4 +41,4 @@ natsort>=2.2.0,<8.2.0
comicapi>=2.2.0,<2.3.0
# Kobo integration
jsonschema>=3.2.0,<4.5.0
jsonschema>=3.2.0,<4.6.0

View File

@ -2,7 +2,7 @@ APScheduler>=3.6.3,<3.10.0
werkzeug<2.1.0
Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.6.1
Flask-Login>=0.3.2,<0.6.2
Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4
Flask>=1.0.2,<2.1.0

File diff suppressed because it is too large Load Diff