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
|
- 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
|
- 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
|
- 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)
|
- Create a custom book collection (shelves)
|
||||||
- Support for editing eBook metadata and deleting eBooks from Calibre library
|
- 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
|
- Support for converting eBooks through Calibre binaries
|
||||||
- Restrict eBook download to logged-in users
|
- Restrict eBook download to logged-in users
|
||||||
- Support for public user registration
|
- 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
|
- 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)
|
- 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)
|
- 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)
|
#### Installation via pip (recommended)
|
||||||
1. To avoid problems with already installed python dependencies, it's recommended to create a virtual environment for Calibre-Web
|
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`).
|
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`
|
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).
|
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 \
|
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
|
||||||
Login with default admin login \
|
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 \
|
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)
|
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:
|
#### Default admin login:
|
||||||
|
@ -64,7 +65,7 @@ Afterwards you can configure your Calibre-Web instance ([Basic Configuration](ht
|
||||||
|
|
||||||
python 3.5+
|
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.
|
[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.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 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.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 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|
|
| 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|
|
||||||
|
|
||||||
|
|
66
cps/about.py
66
cps/about.py
|
@ -25,7 +25,6 @@ import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import werkzeug
|
|
||||||
import flask
|
import flask
|
||||||
import flask_login
|
import flask_login
|
||||||
import jinja2
|
import jinja2
|
||||||
|
@ -37,41 +36,40 @@ from .render_template import render_title_template
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
|
|
||||||
ret = dict()
|
modules = dict()
|
||||||
req = dep_check.load_dependencys(False)
|
req = dep_check.load_dependencies(False)
|
||||||
opt = dep_check.load_dependencys(True)
|
opt = dep_check.load_dependencies(True)
|
||||||
for i in (req + opt):
|
for i in (req + opt):
|
||||||
ret[i[1]] = i[0]
|
modules[i[1]] = i[0]
|
||||||
|
modules['Jinja2'] = jinja2.__version__
|
||||||
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
|
modules['pySqlite'] = sqlite3.version
|
||||||
calibre_web_version = constants.STABLE_VERSION['version']
|
modules['SQLite'] = sqlite3.sqlite_version
|
||||||
else:
|
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
def collect_stats():
|
def collect_stats():
|
||||||
_VERSIONS['ebook converter'] = converter.get_calibre_version()
|
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
|
||||||
_VERSIONS['unrar'] = converter.get_unrar_version()
|
calibre_web_version = constants.STABLE_VERSION['version']
|
||||||
_VERSIONS['kepubify'] = converter.get_kepubify_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
|
return _VERSIONS
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,7 +78,7 @@ def collect_stats():
|
||||||
def stats():
|
def stats():
|
||||||
counter = calibre_db.session.query(db.Books).count()
|
counter = calibre_db.session.query(db.Books).count()
|
||||||
authors = calibre_db.session.query(db.Authors).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()
|
series = calibre_db.session.query(db.Series).count()
|
||||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
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 base64
|
||||||
import json
|
import json
|
||||||
import operator
|
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
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,7 +160,7 @@ def shutdown():
|
||||||
return json.dumps(showtext), 400
|
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
|
# needed for docker applications, as changes on metadata.db from host are not visible to application
|
||||||
@admi.route("/reconnect", methods=['GET'])
|
@admi.route("/reconnect", methods=['GET'])
|
||||||
def reconnect():
|
def reconnect():
|
||||||
|
@ -205,7 +207,7 @@ def admin():
|
||||||
|
|
||||||
all_user = ub.session.query(ub.User).all()
|
all_user = ub.session.query(ub.User).all()
|
||||||
email_settings = config.get_mail_settings()
|
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)
|
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
|
||||||
schedule_duration = format_timedelta(t, threshold=.99)
|
schedule_duration = format_timedelta(t, threshold=.99)
|
||||||
|
|
||||||
|
@ -613,7 +615,8 @@ def load_dialogtexts(element_id):
|
||||||
elif element_id == "db_submit":
|
elif element_id == "db_submit":
|
||||||
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
||||||
elif element_id == "admin_refresh_cover_cache":
|
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":
|
elif element_id == "btnfullsync":
|
||||||
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database "
|
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database "
|
||||||
"to force a full sync with your Kobo Reader?")
|
"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))
|
ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
|
@ -1082,7 +1086,7 @@ def _configuration_gdrive_helper(to_save):
|
||||||
gdrive_secrets['redirect_uris'][0]
|
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)
|
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:
|
if config.config_use_google_drive and not new_gdrive_value:
|
||||||
config.config_google_drive_watch_changes_response = {}
|
config.config_google_drive_watch_changes_response = {}
|
||||||
|
@ -1300,7 +1304,7 @@ def edit_scheduledtasks():
|
||||||
duration_field = list()
|
duration_field = list()
|
||||||
|
|
||||||
for n in range(24):
|
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):
|
for n in range(5, 65, 5):
|
||||||
t = timedelta(hours=n // 60, minutes=n % 60)
|
t = timedelta(hours=n // 60, minutes=n % 60)
|
||||||
duration_field.append((n, format_timedelta(t, threshold=.9)))
|
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)
|
log.warning("LDAP User %s Already in Database", user_data)
|
||||||
return 0, None
|
return 0, None
|
||||||
|
|
||||||
kindlemail = ''
|
ereader_mail = ''
|
||||||
if 'mail' in user_data:
|
if 'mail' in user_data:
|
||||||
useremail = user_data['mail'][0].decode('utf-8')
|
useremail = user_data['mail'][0].decode('utf-8')
|
||||||
if len(user_data['mail']) > 1:
|
if len(user_data['mail']) > 1:
|
||||||
kindlemail = user_data['mail'][1].decode('utf-8')
|
ereader_mail = user_data['mail'][1].decode('utf-8')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug('No Mail Field Found in LDAP Response')
|
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.name = username
|
||||||
content.password = '' # dummy password which will be replaced by ldap one
|
content.password = '' # dummy password which will be replaced by ldap one
|
||||||
content.email = useremail
|
content.email = useremail
|
||||||
content.kindle_mail = kindlemail
|
content.kindle_mail = ereader_mail
|
||||||
content.default_language = config.config_default_language
|
content.default_language = config.config_default_language
|
||||||
content.locale = config.config_default_locale
|
content.locale = config.config_default_locale
|
||||||
content.role = config.config_default_role
|
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")
|
log.info("Missing entries on new user")
|
||||||
raise Exception(_(u"Please fill out all fields!"))
|
raise Exception(_(u"Please fill out all fields!"))
|
||||||
content.email = check_email(to_save["email"])
|
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"])
|
content.name = check_username(to_save["name"])
|
||||||
if to_save.get("kindle_mail"):
|
if to_save.get("kindle_mail"):
|
||||||
content.kindle_mail = valid_email(to_save["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:
|
try:
|
||||||
if to_save.get("email", content.email) != content.email:
|
if to_save.get("email", content.email) != content.email:
|
||||||
content.email = check_email(to_save["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", content.name) != content.name:
|
||||||
if to_save.get("name") == "Guest":
|
if to_save.get("name") == "Guest":
|
||||||
raise Exception(_("Guest Name can't be changed"))
|
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):
|
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:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -32,8 +32,10 @@ def get_locale():
|
||||||
def get_user_locale_language(user_language):
|
def get_user_locale_language(user_language):
|
||||||
return Locale.parse(user_language).get_language_name(get_locale())
|
return Locale.parse(user_language).get_language_name(get_locale())
|
||||||
|
|
||||||
|
|
||||||
def get_available_locale():
|
def get_available_locale():
|
||||||
return [Locale('en')] + babel.list_translations()
|
return [Locale('en')] + babel.list_translations()
|
||||||
|
|
||||||
|
|
||||||
def get_available_translations():
|
def get_available_translations():
|
||||||
return set(str(item) for item in get_available_locale())
|
return set(str(item) for item in get_available_locale())
|
||||||
|
|
|
@ -49,7 +49,7 @@ def init_cache_busting(app):
|
||||||
rooted_filename = os.path.join(dirpath, filename)
|
rooted_filename = os.path.join(dirpath, filename)
|
||||||
try:
|
try:
|
||||||
with open(rooted_filename, 'rb') as f:
|
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
|
# save version to tables
|
||||||
file_path = rooted_filename.replace(static_folder, "")
|
file_path = rooted_filename.replace(static_folder, "")
|
||||||
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
|
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')
|
log.debug('Finished computing cache-busting values')
|
||||||
|
|
||||||
def bust_filename(filename):
|
def bust_filename(file_name):
|
||||||
return hash_table.get(filename, "")
|
return hash_table.get(file_name, "")
|
||||||
|
|
||||||
def unbust_filename(filename):
|
def unbust_filename(file_name):
|
||||||
return filename.split("?", 1)[0]
|
return file_name.split("?", 1)[0]
|
||||||
|
|
||||||
@app.url_defaults
|
@app.url_defaults
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
|
|
19
cps/cli.py
19
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 NIGHTLY_VERSION as _NIGHTLY_VERSION
|
||||||
from .constants import DEFAULT_SETTINGS_FILE, DEFAULT_GDRIVE_FILE
|
from .constants import DEFAULT_SETTINGS_FILE, DEFAULT_GDRIVE_FILE
|
||||||
|
|
||||||
|
|
||||||
def version_info():
|
def version_info():
|
||||||
if _NIGHTLY_VERSION[1].startswith('$Format'):
|
if _NIGHTLY_VERSION[1].startswith('$Format'):
|
||||||
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
|
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
|
||||||
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
|
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
|
||||||
|
|
||||||
|
|
||||||
class CliParameter(object):
|
class CliParameter(object):
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
self.arg_parser()
|
self.arg_parser()
|
||||||
|
|
||||||
def arg_parser(self):
|
def arg_parser(self):
|
||||||
parser = argparse.ArgumentParser(description='Calibre Web is a web app'
|
parser = argparse.ArgumentParser(description='Calibre Web is a web app providing '
|
||||||
' providing a interface for browsing, reading and downloading eBooks\n',
|
'a interface for browsing, reading and downloading eBooks\n',
|
||||||
prog='cps.py')
|
prog='cps.py')
|
||||||
parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db')
|
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('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db')
|
||||||
parser.add_argument('-c', metavar='path',
|
parser.add_argument('-c', metavar='path', help='path and name to SSL certfile, e.g. /opt/test.cert, '
|
||||||
help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile')
|
'works only in combination with keyfile')
|
||||||
parser.add_argument('-k', metavar='path',
|
parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, '
|
||||||
help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile')
|
'works only in combination with certfile')
|
||||||
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
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):
|
if os.path.isdir(self.gd_path):
|
||||||
self.gd_path = os.path.join(self.gd_path, DEFAULT_GDRIVE_FILE)
|
self.gd_path = os.path.join(self.gd_path, DEFAULT_GDRIVE_FILE)
|
||||||
|
|
||||||
|
|
||||||
# handle and check parameter for ssl encryption
|
# handle and check parameter for ssl encryption
|
||||||
self.certfilepath = None
|
self.certfilepath = None
|
||||||
self.keyfilepath = None
|
self.keyfilepath = None
|
||||||
|
@ -96,7 +97,7 @@ class CliParameter(object):
|
||||||
self.keyfilepath = ""
|
self.keyfilepath = ""
|
||||||
|
|
||||||
# dry run updater
|
# dry run updater
|
||||||
self.dry_run =args.d or None
|
self.dry_run = args.d or None
|
||||||
# enable reconnect endpoint for docker database reconnect
|
# enable reconnect endpoint for docker database reconnect
|
||||||
self.reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
|
self.reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
|
||||||
# load covers from localhost
|
# load covers from localhost
|
||||||
|
@ -112,7 +113,7 @@ class CliParameter(object):
|
||||||
else:
|
else:
|
||||||
socket.inet_pton(socket.AF_INET, self.ip_address)
|
socket.inet_pton(socket.AF_INET, self.ip_address)
|
||||||
else:
|
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
|
# inet_atom only handles IPv4 addresses
|
||||||
socket.inet_aton(self.ip_address)
|
socket.inet_aton(self.ip_address)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
|
|
|
@ -35,6 +35,7 @@ from . import constants, logger
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
_Base = declarative_base()
|
_Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class _Flask_Settings(_Base):
|
class _Flask_Settings(_Base):
|
||||||
__tablename__ = 'flask_settings'
|
__tablename__ = 'flask_settings'
|
||||||
|
|
||||||
|
@ -67,14 +68,14 @@ class _Settings(_Base):
|
||||||
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||||
config_certfile = Column(String)
|
config_certfile = Column(String)
|
||||||
config_keyfile = 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_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||||
config_books_per_page = Column(Integer, default=60)
|
config_books_per_page = Column(Integer, default=60)
|
||||||
config_random_books = Column(Integer, default=4)
|
config_random_books = Column(Integer, default=4)
|
||||||
config_authors_max = Column(Integer, default=0)
|
config_authors_max = Column(Integer, default=0)
|
||||||
config_read_column = 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_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_theme = Column(Integer, default=0)
|
||||||
|
|
||||||
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
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_key_path = Column(String, default="")
|
||||||
config_ldap_dn = Column(String, default='dc=example,dc=org')
|
config_ldap_dn = Column(String, default='dc=example,dc=org')
|
||||||
config_ldap_user_object = Column(String, default='uid=%s')
|
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_openldap = Column(Boolean, default=True)
|
||||||
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
|
config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))')
|
||||||
config_ldap_group_members_field = Column(String, default='memberUid')
|
config_ldap_group_members_field = Column(String, default='memberUid')
|
||||||
|
@ -171,7 +172,6 @@ class _ConfigSQL(object):
|
||||||
self.config_converterpath = autodetect_calibre_binary()
|
self.config_converterpath = autodetect_calibre_binary()
|
||||||
|
|
||||||
if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
|
if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
|
||||||
|
|
||||||
change = True
|
change = True
|
||||||
self.config_kepubifypath = autodetect_kepubify_binary()
|
self.config_kepubifypath = autodetect_kepubify_binary()
|
||||||
|
|
||||||
|
@ -257,14 +257,14 @@ class _ConfigSQL(object):
|
||||||
return logger.get_level_name(self.config_log_level)
|
return logger.get_level_name(self.config_log_level)
|
||||||
|
|
||||||
def get_mail_settings(self):
|
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):
|
def get_mail_server_configured(self):
|
||||||
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
|
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))
|
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
|
||||||
|
|
||||||
def get_scheduled_task_settings(self):
|
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):
|
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
|
||||||
"""Possibly updates a field of this object.
|
"""Possibly updates a field of this object.
|
||||||
|
@ -301,7 +301,7 @@ class _ConfigSQL(object):
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
def load(self):
|
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
|
s = self._read_from_storage() # type: _Settings
|
||||||
for k, v in s.__dict__.items():
|
for k, v in s.__dict__.items():
|
||||||
if k[0] != '_':
|
if k[0] != '_':
|
||||||
|
@ -334,7 +334,7 @@ class _ConfigSQL(object):
|
||||||
self._session.rollback()
|
self._session.rollback()
|
||||||
|
|
||||||
def save(self):
|
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
|
s = self._read_from_storage() # type: _Settings
|
||||||
|
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
|
@ -369,6 +369,7 @@ class _ConfigSQL(object):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _migrate_table(session, orm_class):
|
def _migrate_table(session, orm_class):
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
|
@ -390,9 +391,9 @@ def _migrate_table(session, orm_class):
|
||||||
else:
|
else:
|
||||||
column_type = column.type
|
column_type = column.type
|
||||||
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
||||||
column_name,
|
column_name,
|
||||||
column_type,
|
column_type,
|
||||||
column_default))
|
column_default))
|
||||||
log.debug(alter_table)
|
log.debug(alter_table)
|
||||||
session.execute(alter_table)
|
session.execute(alter_table)
|
||||||
changed = True
|
changed = True
|
||||||
|
@ -462,6 +463,7 @@ def load_configuration(conf, session, cli):
|
||||||
conf.init_config(session, cli)
|
conf.init_config(session, cli)
|
||||||
# return conf
|
# return conf
|
||||||
|
|
||||||
|
|
||||||
def get_flask_session_key(_session):
|
def get_flask_session_key(_session):
|
||||||
flask_settings = _session.query(_Flask_Settings).one_or_none()
|
flask_settings = _session.query(_Flask_Settings).one_or_none()
|
||||||
if flask_settings == None:
|
if flask_settings == None:
|
||||||
|
|
|
@ -54,10 +54,9 @@ def get_calibre_version():
|
||||||
def get_unrar_version():
|
def get_unrar_version():
|
||||||
unrar_version = _get_command_version(config.config_rarfile_location, r'UNRAR.*\d')
|
unrar_version = _get_command_version(config.config_rarfile_location, r'UNRAR.*\d')
|
||||||
if unrar_version == "not installed":
|
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
|
return unrar_version
|
||||||
|
|
||||||
|
|
||||||
def get_kepubify_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')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import ast
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
@ -388,7 +387,7 @@ class CustomColumns(Base):
|
||||||
normalized = Column(Boolean)
|
normalized = Column(Boolean)
|
||||||
|
|
||||||
def get_display_dict(self):
|
def get_display_dict(self):
|
||||||
display_dict = ast.literal_eval(self.display)
|
display_dict = json.loads(self.display)
|
||||||
return display_dict
|
return display_dict
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ from .about import collect_stats
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
def assemble_logfiles(file_name):
|
def assemble_logfiles(file_name):
|
||||||
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
|
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
|
||||||
wfd = BytesIO()
|
wfd = BytesIO()
|
||||||
|
|
|
@ -20,7 +20,8 @@ if not importlib:
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
pkgresources = False
|
pkgresources = False
|
||||||
|
|
||||||
def load_dependencys(optional=False):
|
|
||||||
|
def load_dependencies(optional=False):
|
||||||
deps = list()
|
deps = list()
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
pip_installed = os.path.join(BASE_DIR, ".pip_installed")
|
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())
|
res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip())
|
||||||
try:
|
try:
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
dep_version = exe_deps[res.group(1).lower().replace('_','-')]
|
dep_version = exe_deps[res.group(1).lower().replace('_', '-')]
|
||||||
else:
|
else:
|
||||||
if importlib:
|
if importlib:
|
||||||
dep_version = version(res.group(1))
|
dep_version = version(res.group(1))
|
||||||
|
@ -57,38 +58,38 @@ def load_dependencys(optional=False):
|
||||||
|
|
||||||
def dependency_check(optional=False):
|
def dependency_check(optional=False):
|
||||||
d = list()
|
d = list()
|
||||||
deps = load_dependencys(optional)
|
deps = load_dependencies(optional)
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
try:
|
try:
|
||||||
dep_version_int = [int(x) for x in dep[0].split('.')]
|
dep_version_int = [int(x) for x in dep[0].split('.')]
|
||||||
low_check = [int(x) for x in dep[3].split('.')]
|
low_check = [int(x) for x in dep[3].split('.')]
|
||||||
high_check = [int(x) for x in dep[5].split('.')]
|
high_check = [int(x) for x in dep[5].split('.')]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
high_check = None
|
high_check = []
|
||||||
except ValueError:
|
except ValueError:
|
||||||
d.append({'name': dep[1],
|
d.append({'name': dep[1],
|
||||||
'target': "available",
|
'target': "available",
|
||||||
'found': "Not available"
|
'found': "Not available"
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if dep[2].strip() == "==":
|
if dep[2].strip() == "==":
|
||||||
if dep_version_int != low_check:
|
if dep_version_int != low_check:
|
||||||
d.append({'name': dep[1],
|
d.append({'name': dep[1],
|
||||||
'found': dep[0],
|
'found': dep[0],
|
||||||
"target": dep[2] + dep[3]})
|
"target": dep[2] + dep[3]})
|
||||||
continue
|
continue
|
||||||
elif dep[2].strip() == ">=":
|
elif dep[2].strip() == ">=":
|
||||||
if dep_version_int < low_check:
|
if dep_version_int < low_check:
|
||||||
d.append({'name': dep[1],
|
d.append({'name': dep[1],
|
||||||
'found': dep[0],
|
'found': dep[0],
|
||||||
"target": dep[2] + dep[3]})
|
"target": dep[2] + dep[3]})
|
||||||
continue
|
continue
|
||||||
elif dep[2].strip() == ">":
|
elif dep[2].strip() == ">":
|
||||||
if dep_version_int <= low_check:
|
if dep_version_int <= low_check:
|
||||||
d.append({'name': dep[1],
|
d.append({'name': dep[1],
|
||||||
'found': dep[0],
|
'found': dep[0],
|
||||||
"target": dep[2] + dep[3]})
|
"target": dep[2] + dep[3]})
|
||||||
continue
|
continue
|
||||||
if dep[4] and dep[5]:
|
if dep[4] and dep[5]:
|
||||||
if dep[4].strip() == "<":
|
if dep[4].strip() == "<":
|
||||||
|
|
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
|
# 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)
|
book = calibre_db.get_book(book_id)
|
||||||
data = calibre_db.get_book_format(book.id, old_book_format)
|
data = calibre_db.get_book_format(book.id, old_book_format)
|
||||||
file_path = os.path.join(calibre_path, book.path, data.name)
|
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())
|
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
||||||
return error_message
|
return error_message
|
||||||
# read settings and append converter task to queue
|
# read settings and append converter task to queue
|
||||||
if kindle_mail:
|
if ereader_mail:
|
||||||
settings = config.get_mail_settings()
|
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.')
|
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||||
else:
|
else:
|
||||||
settings = dict()
|
settings = dict()
|
||||||
|
@ -104,14 +104,14 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
|
||||||
link)
|
link)
|
||||||
settings['old_book_format'] = old_book_format
|
settings['old_book_format'] = old_book_format
|
||||||
settings['new_book_format'] = new_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
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Texts are not lazy translated as they are supposed to get send out as is
|
# 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,
|
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.')))
|
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -139,26 +139,26 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_send_to_kindle_with_converter(formats):
|
def check_send_to_ereader_with_converter(formats):
|
||||||
book_formats = list()
|
book_formats = list()
|
||||||
if 'EPUB' in formats and 'MOBI' not in formats:
|
if 'MOBI' in formats and 'EPUB' not in formats:
|
||||||
book_formats.append({'format': 'Mobi',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 1,
|
'convert': 1,
|
||||||
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
|
||||||
orig='Epub',
|
orig='Mobi',
|
||||||
format='Mobi')})
|
format='Epub')})
|
||||||
if 'AZW3' in formats and 'MOBI' not in formats:
|
if 'AZW3' in formats and 'EPUB' not in formats:
|
||||||
book_formats.append({'format': 'Mobi',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 2,
|
'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',
|
orig='Azw3',
|
||||||
format='Mobi')})
|
format='Epub')})
|
||||||
return book_formats
|
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()
|
formats = list()
|
||||||
book_formats = list()
|
book_formats = list()
|
||||||
|
@ -166,20 +166,24 @@ def check_send_to_kindle(entry):
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
if ele.uncompressed_size < config.mail_size:
|
if ele.uncompressed_size < config.mail_size:
|
||||||
formats.append(ele.format)
|
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:
|
if 'MOBI' in formats:
|
||||||
book_formats.append({'format': 'Mobi',
|
book_formats.append({'format': 'Mobi',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to Kindle', format='Mobi')})
|
'text': _('Send %(format)s to E-Reader', format='Mobi')})
|
||||||
if 'PDF' in formats:
|
if 'PDF' in formats:
|
||||||
book_formats.append({'format': 'Pdf',
|
book_formats.append({'format': 'Pdf',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
'text': _('Send %(format)s to E-Reader', format='Pdf')})
|
||||||
if 'AZW' in formats:
|
if 'AZW' in formats:
|
||||||
book_formats.append({'format': 'Azw',
|
book_formats.append({'format': 'Azw',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to Kindle', format='Azw')})
|
'text': _('Send %(format)s to E-Reader', format='Azw')})
|
||||||
if config.config_converterpath:
|
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
|
return book_formats
|
||||||
else:
|
else:
|
||||||
log.error(u'Cannot find book entry %d', entry.id)
|
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:
|
# Files are processed in the following order/priority:
|
||||||
# 1: If Mobi file is existing, it's directly send to kindle email,
|
# 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 kindle 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 kindle email
|
# 3: If Pdf file is existing, it's directly send to E-Reader email
|
||||||
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
|
||||||
"""Send email with attachments"""
|
"""Send email with attachments"""
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
|
|
||||||
if convert == 1:
|
if convert == 1:
|
||||||
# returns None if success, otherwise errormessage
|
# 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:
|
if convert == 2:
|
||||||
# returns None if success, otherwise errormessage
|
# 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):
|
for entry in iter(book.data):
|
||||||
if entry.format.upper() == book_format.upper():
|
if entry.format.upper() == book_format.upper():
|
||||||
converted_file_name = entry.name + '.' + book_format.lower()
|
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))
|
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)
|
email_text = N_(u"%(book)s send to E-Reader", book=link)
|
||||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), kindle_mail,
|
config.get_mail_settings(), ereader_mail,
|
||||||
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
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
|
# pipe has to be replaced with comma
|
||||||
value = re.sub(r'[|]+', u',', value, flags=re.U)
|
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('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
|
||||||
value = value.encode(filename_encoding_for_length)[:chars].decode('utf-8', errors='ignore').strip()
|
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
raise ValueError("Filename cannot be empty")
|
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
|
img = advocate.get(url, timeout=(10, 200), allow_redirects=False) # ToDo: Error Handling
|
||||||
else:
|
else:
|
||||||
log.error("python module advocate is not installed but is needed")
|
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()
|
img.raise_for_status()
|
||||||
return save_cover(img, book_path)
|
return save_cover(img, book_path)
|
||||||
except (socket.gaierror,
|
except (socket.gaierror,
|
||||||
|
|
|
@ -54,7 +54,6 @@ class _Logger(logging.Logger):
|
||||||
else:
|
else:
|
||||||
self.error(message, *args, **kwargs)
|
self.error(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def debug_no_auth(self, message, *args, **kwargs):
|
def debug_no_auth(self, message, *args, **kwargs):
|
||||||
message = message.strip("\r\n")
|
message = message.strip("\r\n")
|
||||||
if message.startswith("send: AUTH"):
|
if message.startswith("send: AUTH"):
|
||||||
|
@ -66,6 +65,7 @@ class _Logger(logging.Logger):
|
||||||
def get(name=None):
|
def get(name=None):
|
||||||
return logging.getLogger(name)
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
parent_frame = inspect.stack(0)[1]
|
parent_frame = inspect.stack(0)[1]
|
||||||
if hasattr(parent_frame, 'frame'):
|
if hasattr(parent_frame, 'frame'):
|
||||||
|
@ -75,9 +75,11 @@ def create():
|
||||||
parent_module = inspect.getmodule(parent_frame)
|
parent_module = inspect.getmodule(parent_frame)
|
||||||
return get(parent_module.__name__)
|
return get(parent_module.__name__)
|
||||||
|
|
||||||
|
|
||||||
def is_debug_enabled():
|
def is_debug_enabled():
|
||||||
return logging.root.level <= logging.DEBUG
|
return logging.root.level <= logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
def is_info_enabled(logger):
|
def is_info_enabled(logger):
|
||||||
return logging.getLogger(logger).level <= logging.INFO
|
return logging.getLogger(logger).level <= logging.INFO
|
||||||
|
|
||||||
|
@ -114,10 +116,10 @@ def get_accesslogfile(log_file):
|
||||||
|
|
||||||
|
|
||||||
def setup(log_file, log_level=None):
|
def setup(log_file, log_level=None):
|
||||||
'''
|
"""
|
||||||
Configure the logging output.
|
Configure the logging output.
|
||||||
May be called multiple times.
|
May be called multiple times.
|
||||||
'''
|
"""
|
||||||
log_level = log_level or DEFAULT_LOG_LEVEL
|
log_level = log_level or DEFAULT_LOG_LEVEL
|
||||||
logging.setLoggerClass(_Logger)
|
logging.setLoggerClass(_Logger)
|
||||||
logging.getLogger(__package__).setLevel(log_level)
|
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
|
# avoid spamming the log with debug messages from libraries
|
||||||
r.setLevel(log_level)
|
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:
|
if log_file != LOG_TO_STDERR and log_file != LOG_TO_STDOUT:
|
||||||
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
|
log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE)
|
||||||
|
|
||||||
|
@ -159,13 +161,14 @@ def setup(log_file, log_level=None):
|
||||||
r.removeHandler(h)
|
r.removeHandler(h)
|
||||||
h.close()
|
h.close()
|
||||||
r.addHandler(file_handler)
|
r.addHandler(file_handler)
|
||||||
|
logging.captureWarnings(True)
|
||||||
return "" if log_file == DEFAULT_LOG_FILE else log_file
|
return "" if log_file == DEFAULT_LOG_FILE else log_file
|
||||||
|
|
||||||
|
|
||||||
def create_access_log(log_file, log_name, formatter):
|
def create_access_log(log_file, log_name, formatter):
|
||||||
'''
|
"""
|
||||||
One-time configuration for the web server's access log.
|
One-time configuration for the web server's access log.
|
||||||
'''
|
"""
|
||||||
log_file = _absolute_log_file(log_file, DEFAULT_ACCESS_LOG)
|
log_file = _absolute_log_file(log_file, DEFAULT_ACCESS_LOG)
|
||||||
logging.debug("access log: %s", log_file)
|
logging.debug("access log: %s", log_file)
|
||||||
|
|
||||||
|
@ -182,8 +185,7 @@ def create_access_log(log_file, log_name, formatter):
|
||||||
|
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
access_log.addHandler(file_handler)
|
access_log.addHandler(file_handler)
|
||||||
return access_log, \
|
return access_log, "" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
|
||||||
"" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
|
|
||||||
|
|
||||||
|
|
||||||
# Enable logging of smtp lib debug output
|
# 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
|
# 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.
|
# 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
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,7 @@ class WebServer(object):
|
||||||
output = _readable_listen_address(self.listen_address, self.listen_port)
|
output = _readable_listen_address(self.listen_address, self.listen_port)
|
||||||
log.info('Starting Gevent server on %s', output)
|
log.info('Starting Gevent server on %s', output)
|
||||||
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
||||||
|
error_log=log,
|
||||||
spawn=Pool(), **ssl_args)
|
spawn=Pool(), **ssl_args)
|
||||||
if ssl_args:
|
if ssl_args:
|
||||||
wrap_socket = self.wsgiserver.wrap_socket
|
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.
|
// 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="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");
|
$(".leramslist").insertBefore(".blur-wrapper");
|
||||||
$('ul[aria-labelledby="btnGroupDrop1"]').insertBefore(".blur-wrapper").addClass("leramslist");
|
$('ul[aria-labelledby="btnGroupDrop1"]').insertBefore(".blur-wrapper").addClass("leramslist");
|
||||||
$("#add-to-shelves").insertBefore(".blur-wrapper");
|
$("#add-to-shelves").insertBefore(".blur-wrapper");
|
||||||
|
@ -215,7 +215,7 @@ if ($("body.book").length > 0) {
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#sendbtn2").click(function () {
|
$("#sendbtn2").click(function () {
|
||||||
$(".sendtokindle-drop").toggle();
|
$(".sendtoereader-drop").toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,12 +242,12 @@ if ($("body.book").length > 0) {
|
||||||
|
|
||||||
if ($("#sendbtn2").length > 0) {
|
if ($("#sendbtn2").length > 0) {
|
||||||
position = $("#sendbtn2").offset().left
|
position = $("#sendbtn2").offset().left
|
||||||
if (position + $(".sendtokindle-drop").width() > $(window).width()) {
|
if (position + $(".sendtoereader-drop").width() > $(window).width()) {
|
||||||
positionOff = position + $(".sendtokindle-drop").width() - $(window).width();
|
positionOff = position + $(".sendtoereader-drop").width() - $(window).width();
|
||||||
ribPosition = position - positionOff - 5
|
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 {
|
} 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) {
|
$(document).mouseup(function (e) {
|
||||||
var container = new Array();
|
var container = new Array();
|
||||||
container.push($('ul[aria-labelledby="read-in-browser"]'));
|
container.push($('ul[aria-labelledby="read-in-browser"]'));
|
||||||
container.push($(".sendtokindle-drop"));
|
container.push($(".sendtoereader-drop"));
|
||||||
container.push($(".leramslist"));
|
container.push($(".leramslist"));
|
||||||
container.push($("#add-to-shelves"));
|
container.push($("#add-to-shelves"));
|
||||||
container.push($(".navbar-collapse.collapse.in"));
|
container.push($(".navbar-collapse.collapse.in"));
|
||||||
|
@ -666,7 +666,7 @@ $("#sendbtn").attr({
|
||||||
|
|
||||||
$("#sendbtn2").attr({
|
$("#sendbtn2").attr({
|
||||||
"data-toggle-two": "tooltip",
|
"data-toggle-two": "tooltip",
|
||||||
"title": $("#sendbtn2").text(), // "Send to Kindle",
|
"title": $("#sendbtn2").text(), // "Send to E-Reader",
|
||||||
"data-placement": "bottom",
|
"data-placement": "bottom",
|
||||||
"data-viewport": ".btn-toolbar"
|
"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):
|
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)
|
super(TaskConvert, self).__init__(task_message)
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.book_id = book_id
|
self.book_id = book_id
|
||||||
self.title = ""
|
self.title = ""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.kindle_mail = kindle_mail
|
self.ereader_mail = ereader_mail
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.results = dict()
|
self.results = dict()
|
||||||
|
@ -85,16 +85,16 @@ class TaskConvert(CalibreTask):
|
||||||
# Upload files to gdrive
|
# Upload files to gdrive
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
if self.kindle_mail:
|
if self.ereader_mail:
|
||||||
# if we're sending to kindle after converting, create a one-off task and run it immediately
|
# 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
|
# todo: figure out how to incorporate this into the progress
|
||||||
try:
|
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'],
|
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||||
self.results["path"],
|
self.results["path"],
|
||||||
filename,
|
filename,
|
||||||
self.settings,
|
self.settings,
|
||||||
self.kindle_mail,
|
self.ereader_mail,
|
||||||
EmailText,
|
EmailText,
|
||||||
self.settings['body'],
|
self.settings['body'],
|
||||||
internal=True)
|
internal=True)
|
||||||
|
@ -112,7 +112,7 @@ class TaskConvert(CalibreTask):
|
||||||
|
|
||||||
# check to see if destination format already exists - or if book is in database
|
# 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
|
# 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\
|
if os.path.isfile(file_path + format_new_ext) or\
|
||||||
local_db.get_book_format(self.book_id, self.settings['new_book_format']):
|
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)
|
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||||
|
@ -273,7 +273,7 @@ class TaskConvert(CalibreTask):
|
||||||
return N_("Convert")
|
return N_("Convert")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Convert {} {}".format(self.book_id, self.kindle_mail)
|
return "Convert {} {}".format(self.book_id, self.ereader_mail)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
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>
|
<tr>
|
||||||
<th>{{_('Username')}}</th>
|
<th>{{_('Username')}}</th>
|
||||||
<th>{{_('E-mail Address')}}</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>{{_('Downloads')}}</th>
|
||||||
<th class="hidden-xs ">{{_('Admin')}}</th>
|
<th class="hidden-xs ">{{_('Admin')}}</th>
|
||||||
<th class="hidden-xs hidden-sm">{{_('Password')}}</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>
|
||||||
<div class="col-sm-9 col-lg-9 book-meta">
|
<div class="col-sm-9 col-lg-9 book-meta">
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<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 g.user.role_download() %}
|
||||||
{% if entry.data|length %}
|
{% if entry.data|length %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
|
@ -37,18 +37,18 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.user.kindle_mail and entry.kindle_list %}
|
{% if g.user.kindle_mail and entry.email_share_list %}
|
||||||
{% if entry.kindle_list.__len__() == 1 %}
|
{% if entry.email_share_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>
|
<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 %}
|
{% else %}
|
||||||
<div class="btn-group" role="group">
|
<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">
|
<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>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="send-to-kindle">
|
<ul class="dropdown-menu" aria-labelledby="send-to-ereader">
|
||||||
{% for format in entry.kindle_list %}
|
{% for format in entry.email_share_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>
|
<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%}
|
{%endfor%}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="btn btn-primary char">{{char.char}}</div>
|
<div class="btn btn-primary char">{{char.char}}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{% if entries[0] %}
|
{% if entries[0] %}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if data == "series" %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</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 g.user.role_download() %}
|
||||||
{% if entry.Books.data|length %}
|
{% if entry.Books.data|length %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<table id="libs" class="table">
|
<table id="libs" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{_('Program Library')}}</th>
|
<th>{{_('Program')}}</th>
|
||||||
<th>{{_('Installed Version')}}</th>
|
<th>{{_('Installed Version')}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group">
|
<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 }}">
|
<input type="email" class="form-control" name="kindle_mail" id="kindle_mail" value="{{ content.kindle_mail if content.kindle_mail != None }}">
|
||||||
</div>
|
</div>
|
||||||
{% if not content.role_anonymous() %}
|
{% 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>
|
<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('name', _('Enter Username'), _('Username'), true) }}
|
||||||
{{ user_table_row('email', _('Enter E-mail Address'), _('E-mail Address'), 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_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_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) }}
|
{{ 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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_versions():
|
def get_magick_version():
|
||||||
ret = dict()
|
ret = dict()
|
||||||
if not use_generic_pdf_cover:
|
if not use_generic_pdf_cover:
|
||||||
ret['Image Magick'] = ImageVersion.MAGICK_VERSION
|
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 .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
||||||
from .helper import check_valid_domain, check_email, check_username, \
|
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, \
|
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
|
edit_book_read_status
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .redirect import redirect_back
|
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"])
|
@web.route('/send/<int:book_id>/<book_format>/<int:convert>', methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@download_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():
|
if not config.get_mail_server_configured():
|
||||||
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
|
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
|
||||||
elif current_user.kindle_mail:
|
elif current_user.kindle_mail:
|
||||||
|
@ -1521,7 +1521,7 @@ def show_book(book_id):
|
||||||
|
|
||||||
entry.ordered_authors = calibre_db.order_authors([entry])
|
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.reader_list = check_read_formats(entry)
|
||||||
|
|
||||||
entry.audio_entries = []
|
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
|
comicapi>=2.2.0,<2.3.0
|
||||||
|
|
||||||
# Kobo integration
|
# 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
|
werkzeug<2.1.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<2.1.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
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
backports_abc>=0.4
|
backports_abc>=0.4
|
||||||
Flask>=1.0.2,<2.1.0
|
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