mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-22 16:10:30 +00:00
Merge branch 'master' into Develop
This commit is contained in:
commit
909797dc49
11
README.md
Normal file → Executable file
11
README.md
Normal file → Executable 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.
|
||||
|
||||
|
@ -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|
|
||||
|
||||
|
44
cps/about.py
44
cps/about.py
@ -25,7 +25,6 @@ import platform
|
||||
import sqlite3
|
||||
from collections import OrderedDict
|
||||
|
||||
import werkzeug
|
||||
import flask
|
||||
import flask_login
|
||||
import jinja2
|
||||
@ -37,12 +36,18 @@ 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]
|
||||
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():
|
||||
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
|
||||
calibre_web_version = constants.STABLE_VERSION['version']
|
||||
else:
|
||||
@ -55,23 +60,16 @@ if getattr(sys, 'frozen', False):
|
||||
elif constants.HOME_CONFIG:
|
||||
calibre_web_version += " - pyPi"
|
||||
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform='{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
_VERSIONS = {'Calibre Web': calibre_web_version}
|
||||
_VERSIONS.update(OrderedDict(
|
||||
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())
|
||||
|
||||
|
||||
def collect_stats():
|
||||
_VERSIONS['ebook converter'] = converter.get_calibre_version()
|
||||
_VERSIONS['unrar'] = converter.get_unrar_version()
|
||||
_VERSIONS['kepubify'] = converter.get_kepubify_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
28
cps/admin.py
Normal file → Executable 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:
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
17
cps/cli.py
17
cps/cli.py
@ -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
|
||||
@ -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:
|
||||
|
@ -35,6 +35,7 @@ from . import constants, logger
|
||||
log = logger.create()
|
||||
_Base = declarative_base()
|
||||
|
||||
|
||||
class _Flask_Settings(_Base):
|
||||
__tablename__ = 'flask_settings'
|
||||
|
||||
@ -74,7 +75,7 @@ class _Settings(_Base):
|
||||
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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
@ -57,7 +57,6 @@ def get_unrar_version():
|
||||
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')
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
@ -57,14 +58,14 @@ 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",
|
||||
|
71
cps/helper.py
Normal file → Executable file
71
cps/helper.py
Normal file → Executable 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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
16
cps/static/js/caliBlur.js
Normal file → Executable 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
0
cps/static/js/main.js
Executable file → Normal file
16
cps/tasks/convert.py
Normal file → Executable file
16
cps/tasks/convert.py
Normal file → Executable 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
2
cps/templates/admin.html
Normal file → Executable 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
16
cps/templates/detail.html
Normal file → Executable 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>
|
||||
|
@ -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] %}
|
||||
|
@ -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
2
cps/templates/shelfdown.html
Normal file → Executable 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">
|
||||
|
@ -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
2
cps/templates/user_edit.html
Normal file → Executable 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
2
cps/templates/user_table.html
Normal file → Executable 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) }}
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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
6
cps/web.py
Normal file → Executable 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 = []
|
||||
|
1643
messages.pot
1643
messages.pot
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user