Merge branch 'master' into Develop

# Conflicts:
#	cps/admin.py
#	cps/config_sql.py
#	cps/search.py
#	cps/templates/admin.html
#	cps/web.py
#	setup.cfg
#	test/Calibre-Web TestSummary_Linux.html
pull/2725/head
Ozzie Isaacs 1 year ago
commit 508e2b4d0a

@ -0,0 +1 @@
onLmA_LND5S8jNSvi8nNSGwevE13f7t8pW-wgWAXZgo=

@ -26,9 +26,9 @@ The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/
Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com". Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com".
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki). Ensure the **bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue. If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new/choose). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
### **Feature Request** ### **Feature Request**

@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine-grained per-user permissions - User management with fine-grained per-user permissions
- 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, galician, german, greek, hungarian, indonesian, italian, japanese, khmer, korean, norwegian, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian, vietnamese
- OPDS feed for eBook reader apps - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series, book format 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)
@ -65,12 +65,15 @@ Afterwards you can configure your Calibre-Web instance ([Basic Configuration](ht
python 3.5+ python 3.5+
[Download](https://imagemagick.org/script/download.php) Imagemagick to extract covers from epubs. On Windows the additional installation of [ghostscript](https://ghostscript.com/releases/gsdnld.html) might be necessary to extract covers from pdf files. On Linux Imagemagick and Ghostscript can often be installed using the system package manager.
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: 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.
[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `/opt/kepubify` Windows: `C:\Program Files\kepubify`. [Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `/opt/kepubify` Windows: `C:\Program Files\kepubify`.
## Docker Images ## Docker Images
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team): A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):

@ -1,3 +1,4 @@
[python: **.py] [python: **.py]
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
[jinja2: **/templates/**.*ml] [jinja2: **/templates/**.*ml]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

@ -37,7 +37,7 @@ from .reverseproxy import ReverseProxied
from .server import WebServer from .server import WebServer
from .dep_check import dependency_check from .dep_check import dependency_check
from .updater import Updater from .updater import Updater
from .babel import babel from .babel import babel, get_locale
from . import config_sql from . import config_sql
from . import cache_buster from . import cache_buster
from . import ub, db from . import ub, db
@ -147,7 +147,7 @@ def create_app():
web_server.stop(True) web_server.stop(True)
sys.exit(7) sys.exit(7)
for res in dependency_check() + dependency_check(True): for res in dependency_check() + dependency_check(True):
log.info('*** "{}" version does not fit the requirements. ' log.info('*** "{}" version does not meet the requirements. '
'Should: {}, Found: {}, please consider installing required version ***' 'Should: {}, Found: {}, please consider installing required version ***'
.format(res['name'], .format(res['name'],
res['target'], res['target'],
@ -164,8 +164,11 @@ def create_app():
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config) web_server.init_app(app, config)
if hasattr(babel, "localeselector"):
babel.init_app(app) babel.init_app(app)
babel.localeselector(get_locale)
else:
babel.init_app(app, locale_selector=get_locale)
from . import services from . import services

@ -81,4 +81,4 @@ def stats():
categories = 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=categories, seriecounter=series, title=_(u"Statistics"), page="stat") categorycounter=categories, seriecounter=series, title=_("Statistics"), page="stat")

@ -26,15 +26,16 @@ import base64
import json import json
import operator import operator
import time import time
import sys
import string
from datetime import datetime, timedelta from datetime import datetime, timedelta
from datetime import time as datetime_time from datetime import time as datetime_time
from functools import wraps from functools import wraps
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask_login import login_required, current_user, logout_user, confirm_login from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import get_locale, format_time, format_datetime, format_timedelta from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from flask import session as flask_session from flask import session as flask_session
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
@ -52,27 +53,28 @@ from .services.worker import WorkerThread
from .babel import get_available_translations, get_available_locale, get_user_locale_language from .babel import get_available_translations, get_available_locale, get_user_locale_language
from . import debug_info from . import debug_info
log = logger.create() log = logger.create()
feature_support = { feature_support = {
'ldap': bool(services.ldap), 'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support), 'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo), 'kobo': bool(services.kobo),
'updater': constants.UPDATER_AVAILABLE, 'updater': constants.UPDATER_AVAILABLE,
'gmail': bool(services.gmail), 'gmail': bool(services.gmail),
'scheduler': schedule.use_APScheduler, 'scheduler': schedule.use_APScheduler,
'gdrive': gdrive_support 'gdrive': gdrive_support
} }
try: try:
import rarfile # pylint: disable=unused-import import rarfile # pylint: disable=unused-import
feature_support['rar'] = True feature_support['rar'] = True
except (ImportError, SyntaxError): except (ImportError, SyntaxError):
feature_support['rar'] = False feature_support['rar'] = False
try: try:
from .oauth_bb import oauth_check, oauthblueprints from .oauth_bb import oauth_check, oauthblueprints
feature_support['oauth'] = True feature_support['oauth'] = True
except ImportError as err: except ImportError as err:
log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err) log.debug('Cannot import Flask-Dance, login with Oauth will not work: %s', err)
@ -80,7 +82,6 @@ except ImportError as err:
oauthblueprints = [] oauthblueprints = []
oauth_check = {} oauth_check = {}
admi = Blueprint('admin', __name__) admi = Blueprint('admin', __name__)
@ -107,6 +108,7 @@ def before_request():
logout_user() logout_user()
g.constants = constants g.constants = constants
g.user = current_user g.user = current_user
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
g.allow_registration = config.config_public_reg g.allow_registration = config.config_public_reg
g.allow_anonymous = config.config_anonbrowse g.allow_anonymous = config.config_anonbrowse
g.allow_upload = config.config_uploading g.allow_upload = config.config_uploading
@ -137,28 +139,39 @@ def admin_forbidden():
@admin_required @admin_required
def shutdown(): def shutdown():
task = request.get_json().get('parameter', -1) task = request.get_json().get('parameter', -1)
showtext = {} show_text = {}
if task in (0, 1): # valid commandos received if task in (0, 1): # valid commandos received
# close all database connections # close all database connections
calibre_db.dispose() calibre_db.dispose()
ub.dispose() ub.dispose()
if task == 0: if task == 0:
showtext['text'] = _(u'Server restarted, please reload page') show_text['text'] = _('Server restarted, please reload page.')
else: else:
showtext['text'] = _(u'Performing shutdown of server, please close window') show_text['text'] = _('Performing Server shutdown, please close window.')
# stop gevent/tornado server # stop gevent/tornado server
web_server.stop(task == 0) web_server.stop(task == 0)
return json.dumps(showtext) return json.dumps(show_text)
if task == 2: if task == 2:
log.warning("reconnecting to calibre database") log.warning("reconnecting to calibre database")
calibre_db.reconnect_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
showtext['text'] = _(u'Reconnect successful') show_text['text'] = _('Success! Database Reconnected')
return json.dumps(showtext) return json.dumps(show_text)
showtext['text'] = _(u'Unknown command') show_text['text'] = _('Unknown command')
return json.dumps(showtext), 400 return json.dumps(show_text), 400
@admi.route("/metadata_backup", methods=["POST"])
@login_required
@admin_required
def queue_metadata_backup():
show_text = {}
log.warning("Queuing all books for metadata backup")
helper.set_all_metadata_dirty()
show_text['text'] = _('Success! Books queued for Metadata Backup')
return json.dumps(show_text)
# method is available without login and not protected by CSRF to make it easy reachable, is per default switched off # method is available without login and not protected by CSRF to make it easy reachable, is per default switched off
@ -190,14 +203,14 @@ def update_thumbnails():
def admin(): def admin():
version = updater_thread.get_current_version_info() version = updater_thread.get_current_version_info()
if version is False: if version is False:
commit = _(u'Unknown') commit = _('Unknown')
else: else:
if 'datetime' in version: if 'datetime' in version:
commit = version['datetime'] commit = version['datetime']
tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S") form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S")
if len(commit) > 19: # check if string has timezone if len(commit) > 19: # check if string has timezone
if commit[19] == '+': if commit[19] == '+':
form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
elif commit[19] == '-': elif commit[19] == '-':
@ -215,7 +228,7 @@ def admin():
return render_title_template("admin.html", allUser=all_user, config=config, commit=commit, return render_title_template("admin.html", allUser=all_user, config=config, commit=commit,
feature_support=feature_support, schedule_time=schedule_time, feature_support=feature_support, schedule_time=schedule_time,
schedule_duration=schedule_duration, schedule_duration=schedule_duration,
title=_(u"Admin page"), page="admin") title=_("Admin page"), page="admin")
@admi.route("/admin/dbconfig", methods=["GET", "POST"]) @admi.route("/admin/dbconfig", methods=["GET", "POST"])
@ -235,7 +248,7 @@ def configuration():
config=config, config=config,
provider=oauthblueprints, provider=oauthblueprints,
feature_support=feature_support, feature_support=feature_support,
title=_(u"Basic Configuration"), page="config") title=_("Basic Configuration"), page="config")
@admi.route("/admin/ajaxconfig", methods=["POST"]) @admi.route("/admin/ajaxconfig", methods=["POST"])
@ -263,9 +276,9 @@ def calibreweb_alive():
@login_required @login_required
@admin_required @admin_required
def view_configuration(): def view_configuration():
read_column = calibre_db.session.query(db.CustomColumns)\ read_column = calibre_db.session.query(db.CustomColumns) \
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all() .filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all()
restrict_columns = calibre_db.session.query(db.CustomColumns)\ restrict_columns = calibre_db.session.query(db.CustomColumns) \
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all() .filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all()
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
translations = get_available_locale() translations = get_available_locale()
@ -273,7 +286,7 @@ def view_configuration():
restrictColumns=restrict_columns, restrictColumns=restrict_columns,
languages=languages, languages=languages,
translations=translations, translations=translations,
title=_(u"UI Configuration"), page="uiconfig") title=_("UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable") @admi.route("/admin/usertable")
@ -284,11 +297,11 @@ def edit_user_table():
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
translations = get_available_locale() translations = get_available_locale()
all_user = ub.session.query(ub.User) all_user = ub.session.query(ub.User)
tags = calibre_db.session.query(db.Tags)\ tags = calibre_db.session.query(db.Tags) \
.join(db.books_tags_link)\ .join(db.books_tags_link) \
.join(db.Books)\ .join(db.Books) \
.filter(calibre_db.common_filters()) \ .filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\ .group_by(text('books_tags_link.tag')) \
.order_by(db.Tags.name).all() .order_by(db.Tags.name).all()
if config.config_restricted_column: if config.config_restricted_column:
custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all() custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all()
@ -307,7 +320,7 @@ def edit_user_table():
all_roles=constants.ALL_ROLES, all_roles=constants.ALL_ROLES,
kobo_support=kobo_support, kobo_support=kobo_support,
sidebar_settings=constants.sidebar_settings, sidebar_settings=constants.sidebar_settings,
title=_(u"Edit Users"), title=_("Edit Users"),
page="usertable") page="usertable")
@ -465,20 +478,20 @@ def edit_list_user(param):
elif param.endswith('role'): elif param.endswith('role'):
value = int(vals['field_index']) value = int(vals['field_index'])
if user.name == "Guest" and value in \ if user.name == "Guest" and value in \
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
raise Exception(_("Guest can't have this role")) raise Exception(_("Guest can't have this role"))
# check for valid value, last on checks for power of 2 value # check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1): if value > 0 and value <= constants.ROLE_VIEWER and (value & value - 1 == 0 or value == 1):
if vals['value'] == 'true': if vals['value'] == 'true':
user.role |= value user.role |= value
elif vals['value'] == 'false': elif vals['value'] == 'false':
if value == constants.ROLE_ADMIN: if value == constants.ROLE_ADMIN:
if not ub.session.query(ub.User).\ if not ub.session.query(ub.User). \
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != user.id).count(): ub.User.id != user.id).count():
return Response( return Response(
json.dumps([{'type': "danger", json.dumps([{'type': "danger",
'message': _(u"No admin user remaining, can't remove admin role", 'message': _("No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json') nick=user.name)}]), mimetype='application/json')
user.role &= ~value user.role &= ~value
else: else:
@ -490,7 +503,7 @@ def edit_list_user(param):
if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD: if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD:
raise Exception(_("Guest can't have this view")) raise Exception(_("Guest can't have this view"))
# check for valid value, last on checks for power of 2 value # check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1): if value > 0 and value <= constants.SIDEBAR_LIST and (value & value - 1 == 0 or value == 1):
if vals['value'] == 'true': if vals['value'] == 'true':
user.sidebar_view |= value user.sidebar_view |= value
elif vals['value'] == 'false': elif vals['value'] == 'false':
@ -555,13 +568,13 @@ def update_view_configuration():
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
if not check_valid_read_column(to_save.get("config_read_column", "0")): if not check_valid_read_column(to_save.get("config_read_column", "0")):
flash(_(u"Invalid Read Column"), category="error") flash(_("Invalid Read Column"), category="error")
log.debug("Invalid Read column") log.debug("Invalid Read column")
return view_configuration() return view_configuration()
_config_int(to_save, "config_read_column") _config_int(to_save, "config_read_column")
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")): if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
flash(_(u"Invalid Restricted Column"), category="error") flash(_("Invalid Restricted Column"), category="error")
log.debug("Invalid Restricted Column") log.debug("Invalid Restricted Column")
return view_configuration() return view_configuration()
_config_int(to_save, "config_restricted_column") _config_int(to_save, "config_restricted_column")
@ -581,7 +594,7 @@ def update_view_configuration():
config.config_default_show |= constants.DETAIL_RANDOM config.config_default_show |= constants.DETAIL_RANDOM
config.save() config.save()
flash(_(u"Calibre-Web configuration updated"), category="success") flash(_("Calibre-Web configuration updated"), category="success")
log.debug("Calibre-Web configuration updated") log.debug("Calibre-Web configuration updated")
before_request() before_request()
@ -643,7 +656,7 @@ def edit_domain(allow):
@admin_required @admin_required
def add_domain(allow): def add_domain(allow):
domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower() domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower()
check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name)\ check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name) \
.filter(ub.Registration.allow == allow).first() .filter(ub.Registration.allow == allow).first()
if not check: if not check:
new_domain = ub.Registration(domain=domain_name, allow=allow) new_domain = ub.Registration(domain=domain_name, allow=allow)
@ -861,16 +874,16 @@ def delete_restriction(res_type, user_id):
@login_required @login_required
@admin_required @admin_required
def list_restriction(res_type, user_id): def list_restriction(res_type, user_id):
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(config.list_denied_tags()) if x != ''] for i, x in enumerate(config.list_denied_tags()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(config.list_allowed_tags()) if x != ''] for i, x in enumerate(config.list_allowed_tags()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
elif res_type == 1: # CustomC as template elif res_type == 1: # CustomC as template
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(config.list_denied_column_values()) if x != ''] for i, x in enumerate(config.list_denied_column_values()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(config.list_allowed_column_values()) if x != ''] for i, x in enumerate(config.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
elif res_type == 2: # Tags per user elif res_type == 2: # Tags per user
@ -878,9 +891,9 @@ def list_restriction(res_type, user_id):
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first() usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
else: else:
usr = current_user usr = current_user
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(usr.list_denied_tags()) if x != ''] for i, x in enumerate(usr.list_denied_tags()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(usr.list_allowed_tags()) if x != ''] for i, x in enumerate(usr.list_allowed_tags()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
elif res_type == 3: # CustomC per user elif res_type == 3: # CustomC per user
@ -888,9 +901,9 @@ def list_restriction(res_type, user_id):
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first() usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
else: else:
usr = current_user usr = current_user
restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd'+str(i)} restrict = [{'Element': x, 'type': _('Deny'), 'id': 'd' + str(i)}
for i, x in enumerate(usr.list_denied_column_values()) if x != ''] for i, x in enumerate(usr.list_denied_column_values()) if x != '']
allow = [{'Element': x, 'type': _('Allow'), 'id': 'a'+str(i)} allow = [{'Element': x, 'type': _('Allow'), 'id': 'a' + str(i)}
for i, x in enumerate(usr.list_allowed_column_values()) if x != ''] for i, x in enumerate(usr.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
else: else:
@ -920,7 +933,7 @@ def ajax_pathchooser():
def check_valid_read_column(column): def check_valid_read_column(column):
if column != "0": if column != "0":
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \ if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all(): .filter(and_(db.CustomColumns.datatype == 'bool', db.CustomColumns.mark_for_delete == 0)).all():
return False return False
return True return True
@ -928,7 +941,7 @@ def check_valid_read_column(column):
def check_valid_restricted_column(column): def check_valid_restricted_column(column):
if column != "0": if column != "0":
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \ if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
.filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all(): .filter(and_(db.CustomColumns.datatype == 'text', db.CustomColumns.mark_for_delete == 0)).all():
return False return False
return True return True
@ -956,7 +969,7 @@ def prepare_tags(user, action, tags_name, id_list):
raise Exception(_("Tag not found")) raise Exception(_("Tag not found"))
new_tags_list = [x.name for x in tags] new_tags_list = [x.name for x in tags]
else: else:
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\ tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column]) \
.filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all() .filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all()
new_tags_list = [x.value for x in tags] new_tags_list = [x.value for x in tags]
saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else [] saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else []
@ -969,6 +982,19 @@ def prepare_tags(user, action, tags_name, id_list):
return ",".join(saved_tags_list) return ",".join(saved_tags_list)
def get_drives(current):
drive_letters = []
for d in string.ascii_uppercase:
if os.path.exists('{}:'.format(d)) and current[0].lower() != d.lower():
drive = "{}:\\".format(d)
data = {"name": drive, "fullpath": drive}
data["sort"] = "_" + data["fullpath"].lower()
data["type"] = "dir"
data["size"] = ""
drive_letters.append(data)
return drive_letters
def pathchooser(): def pathchooser():
browse_for = "folder" browse_for = "folder"
folder_only = request.args.get('folder', False) == "true" folder_only = request.args.get('folder', False) == "true"
@ -976,40 +1002,41 @@ def pathchooser():
path = os.path.normpath(request.args.get('path', "")) path = os.path.normpath(request.args.get('path', ""))
if os.path.isfile(path): if os.path.isfile(path):
oldfile = path old_file = path
path = os.path.dirname(path) path = os.path.dirname(path)
else: else:
oldfile = "" old_file = ""
absolute = False absolute = False
if os.path.isdir(path): if os.path.isdir(path):
# if os.path.isabs(path):
cwd = os.path.realpath(path) cwd = os.path.realpath(path)
absolute = True absolute = True
# else:
# cwd = os.path.relpath(path)
else: else:
cwd = os.getcwd() cwd = os.getcwd()
cwd = os.path.normpath(os.path.realpath(cwd)) cwd = os.path.normpath(os.path.realpath(cwd))
parentdir = os.path.dirname(cwd) parent_dir = os.path.dirname(cwd)
if not absolute: if not absolute:
if os.path.realpath(cwd) == os.path.realpath("/"): if os.path.realpath(cwd) == os.path.realpath("/"):
cwd = os.path.relpath(cwd) cwd = os.path.relpath(cwd)
else: else:
cwd = os.path.relpath(cwd) + os.path.sep cwd = os.path.relpath(cwd) + os.path.sep
parentdir = os.path.relpath(parentdir) + os.path.sep parent_dir = os.path.relpath(parent_dir) + os.path.sep
if os.path.realpath(cwd) == os.path.realpath("/"): files = []
parentdir = "" if os.path.realpath(cwd) == os.path.realpath("/") \
or (sys.platform == "win32" and os.path.realpath(cwd)[1:] == os.path.realpath("/")[1:]):
# we are in root
parent_dir = ""
if sys.platform == "win32":
files = get_drives(cwd)
try: try:
folders = os.listdir(cwd) folders = os.listdir(cwd)
except Exception: except Exception:
folders = [] folders = []
files = []
for f in folders: for f in folders:
try: try:
data = {"name": f, "fullpath": os.path.join(cwd, f)} data = {"name": f, "fullpath": os.path.join(cwd, f)}
@ -1042,9 +1069,9 @@ def pathchooser():
context = { context = {
"cwd": cwd, "cwd": cwd,
"files": files, "files": files,
"parentdir": parentdir, "parentdir": parent_dir,
"type": browse_for, "type": browse_for,
"oldfile": oldfile, "oldfile": old_file,
"absolute": absolute, "absolute": absolute,
} }
return json.dumps(context) return json.dumps(context)
@ -1082,10 +1109,10 @@ def _configuration_gdrive_helper(to_save):
if not gdrive_secrets: if not gdrive_secrets:
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application')) return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings( gdriveutils.update_settings(
gdrive_secrets['client_id'], gdrive_secrets['client_id'],
gdrive_secrets['client_secret'], gdrive_secrets['client_secret'],
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)
@ -1102,12 +1129,12 @@ def _configuration_oauth_helper(to_save):
reboot_required = False reboot_required = False
for element in oauthblueprints: for element in oauthblueprints:
if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \ if to_save["config_" + str(element['id']) + "_oauth_client_id"] != element['oauth_client_id'] \
or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']: or to_save["config_" + str(element['id']) + "_oauth_client_secret"] != element['oauth_client_secret']:
reboot_required = True reboot_required = True
element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"] element['oauth_client_id'] = to_save["config_" + str(element['id']) + "_oauth_client_id"]
element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"] element['oauth_client_secret'] = to_save["config_" + str(element['id']) + "_oauth_client_secret"]
if to_save["config_" + str(element['id']) + "_oauth_client_id"] \ if to_save["config_" + str(element['id']) + "_oauth_client_id"] \
and to_save["config_" + str(element['id']) + "_oauth_client_secret"]: and to_save["config_" + str(element['id']) + "_oauth_client_secret"]:
active_oauths += 1 active_oauths += 1
element["active"] = 1 element["active"] = 1
else: else:
@ -1160,7 +1187,7 @@ def _configuration_ldap_helper(to_save):
if not config.config_ldap_provider_url \ if not config.config_ldap_provider_url \
or not config.config_ldap_port \ or not config.config_ldap_port \
or not config.config_ldap_dn \ or not config.config_ldap_dn \
or not config.config_ldap_user_object: or not config.config_ldap_user_object:
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, ' return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier')) 'Port, DN and User Object Identifier'))
@ -1230,7 +1257,7 @@ def new_user():
content.default_language = config.config_default_language content.default_language = config.config_default_language
return render_title_template("user_edit.html", new_user=1, content=content, return render_title_template("user_edit.html", new_user=1, content=content,
config=config, translations=translations, config=config, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser", languages=languages, title=_("Add New User"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
@ -1239,7 +1266,7 @@ def new_user():
@admin_required @admin_required
def edit_mailsettings(): def edit_mailsettings():
content = config.get_mail_settings() content = config.get_mail_settings()
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"), return render_title_template("email_edit.html", content=content, title=_("Edit Email Server Settings"),
page="mailset", feature_support=feature_support) page="mailset", feature_support=feature_support)
@ -1258,7 +1285,7 @@ def update_mailsettings():
elif to_save.get("gmail"): elif to_save.get("gmail"):
try: try:
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token) config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
flash(_(u"Gmail Account Verification Successful"), category="success") flash(_("Success! Gmail Account Verified."), category="success")
except Exception as ex: except Exception as ex:
flash(str(ex), category="error") flash(str(ex), category="error")
log.error(ex) log.error(ex)
@ -1268,34 +1295,33 @@ def update_mailsettings():
_config_int(to_save, "mail_port") _config_int(to_save, "mail_port")
_config_int(to_save, "mail_use_ssl") _config_int(to_save, "mail_use_ssl")
_config_string(to_save, "mail_password_e") _config_string(to_save, "mail_password_e")
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) _config_int(to_save, "mail_size", lambda y: int(y) * 1024 * 1024)
_config_string(to_save, "mail_server") config.mail_server = to_save.get('mail_server', "").strip()
_config_string(to_save, "mail_from") config.mail_from = to_save.get('mail_from', "").strip()
_config_string(to_save, "mail_login") config.mail_login = to_save.get('mail_login', "").strip()
try: try:
config.save() config.save()
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return edit_mailsettings() return edit_mailsettings()
except Exception as e: except Exception as e:
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return edit_mailsettings() return edit_mailsettings()
if to_save.get("test"): if to_save.get("test"):
if current_user.email: if current_user.email:
result = send_test_mail(current_user.email, current_user.name) result = send_test_mail(current_user.email, current_user.name)
if result is None: if result is None:
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result", flash(_("Test e-mail queued for sending to %(email)s, please check Tasks for result",
email=current_user.email), category="info") email=current_user.email), category="info")
else: else:
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") flash(_("There was an error sending the Test e-mail: %(res)s", res=result), category="error")
else: else:
flash(_(u"Please configure your e-mail address first..."), category="error") flash(_("Please configure your e-mail address first..."), category="error")
else: else:
flash(_(u"E-mail server settings updated"), category="success") flash(_("Email Server Settings updated"), category="success")
return edit_mailsettings() return edit_mailsettings()
@ -1309,16 +1335,16 @@ 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(datetime_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=.97)))
return render_title_template("schedule_edit.html", return render_title_template("schedule_edit.html",
config=content, config=content,
starttime=time_field, starttime=time_field,
duration=duration_field, duration=duration_field,
title=_(u"Edit Scheduled Tasks Settings")) title=_("Edit Scheduled Tasks Settings"))
@admi.route("/admin/scheduledtasks", methods=["POST"]) @admi.route("/admin/scheduledtasks", methods=["POST"])
@ -1330,12 +1356,12 @@ def update_scheduledtasks():
if 0 <= int(to_save.get("schedule_start_time")) <= 23: if 0 <= int(to_save.get("schedule_start_time")) <= 23:
_config_int( to_save, "schedule_start_time") _config_int( to_save, "schedule_start_time")
else: else:
flash(_(u"Invalid start time for task specified"), category="error") flash(_("Invalid start time for task specified"), category="error")
error = True error = True
if 0 < int(to_save.get("schedule_duration")) <= 60: if 0 < int(to_save.get("schedule_duration")) <= 60:
_config_int(to_save, "schedule_duration") _config_int(to_save, "schedule_duration")
else: else:
flash(_(u"Invalid duration for task specified"), category="error") flash(_("Invalid duration for task specified"), category="error")
error = True error = True
_config_checkbox(to_save, "schedule_generate_book_covers") _config_checkbox(to_save, "schedule_generate_book_covers")
_config_checkbox(to_save, "schedule_generate_series_covers") _config_checkbox(to_save, "schedule_generate_series_covers")
@ -1344,7 +1370,7 @@ def update_scheduledtasks():
if not error: if not error:
try: try:
config.save() config.save()
flash(_(u"Scheduled tasks settings updated"), category="success") flash(_("Scheduled tasks settings updated"), category="success")
# Cancel any running tasks # Cancel any running tasks
schedule.end_scheduled_tasks() schedule.end_scheduled_tasks()
@ -1354,7 +1380,7 @@ def update_scheduledtasks():
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
log.error("An unknown error occurred while saving scheduled tasks settings") log.error("An unknown error occurred while saving scheduled tasks settings")
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
except OperationalError: except OperationalError:
ub.session.rollback() ub.session.rollback()
log.error("Settings DB is not Writeable") log.error("Settings DB is not Writeable")
@ -1369,7 +1395,7 @@ def update_scheduledtasks():
def edit_user(user_id): def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
if not content or (not config.config_anonbrowse and content.name == "Guest"): if not content or (not config.config_anonbrowse and content.name == "Guest"):
flash(_(u"User not found"), category="error") flash(_("User not found"), category="error")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
languages = calibre_db.speaking_language(return_all_languages=True) languages = calibre_db.speaking_language(return_all_languages=True)
translations = get_available_locale() translations = get_available_locale()
@ -1388,7 +1414,7 @@ def edit_user(user_id):
registered_oauth=oauth_check, registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(), mail_configured=config.get_mail_server_configured(),
kobo_support=kobo_support, kobo_support=kobo_support,
title=_(u"Edit User %(nick)s", nick=content.name), title=_("Edit User %(nick)s", nick=content.name),
page="edituser") page="edituser")
@ -1399,14 +1425,14 @@ def reset_user_password(user_id):
if current_user is not None and current_user.is_authenticated: if current_user is not None and current_user.is_authenticated:
ret, message = reset_password(user_id) ret, message = reset_password(user_id)
if ret == 1: if ret == 1:
log.debug(u"Password for user %s reset", message) log.debug("Password for user %s reset", message)
flash(_(u"Password for user %(user)s reset", user=message), category="success") flash(_("Success! Password for user %(user)s reset", user=message), category="success")
elif ret == 0: elif ret == 0:
log.error(u"An unknown error occurred. Please try again later.") log.error("An unknown error occurred. Please try again later.")
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
else: else:
log.error(u"Please configure the SMTP mail settings first...") log.error("Please configure the SMTP mail settings.")
flash(_(u"Please configure the SMTP mail settings first..."), category="error") flash(_("Oops! Please configure the SMTP mail settings."), category="error")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
@ -1417,7 +1443,7 @@ def view_logfile():
logfiles = {0: logger.get_logfile(config.config_logfile), logfiles = {0: logger.get_logfile(config.config_logfile),
1: logger.get_accesslogfile(config.config_access_logfile)} 1: logger.get_accesslogfile(config.config_access_logfile)}
return render_title_template("logviewer.html", return render_title_template("logviewer.html",
title=_(u"Logfile viewer"), title=_("Logfile viewer"),
accesslog_enable=config.config_access_log, accesslog_enable=config.config_access_log,
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT), log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
logfiles=logfiles, logfiles=logfiles,
@ -1467,7 +1493,7 @@ def download_debug():
@admin_required @admin_required
def get_update_status(): def get_update_status():
if feature_support['updater']: if feature_support['updater']:
log.info(u"Update status requested") log.info("Update status requested")
return updater_thread.get_available_updates(request.method) return updater_thread.get_available_updates(request.method)
else: else:
return '' return ''
@ -1560,7 +1586,7 @@ def ldap_import_create_user(user, user_data):
ub.session.add(content) ub.session.add(content)
try: try:
ub.session.commit() ub.session.commit()
return 1, None # increase no of users return 1, None # increase no of users
except Exception as ex: except Exception as ex:
log.warning("Failed to create LDAP user: %s - %s", user, ex) log.warning("Failed to create LDAP user: %s - %s", user, ex)
ub.session.rollback() ub.session.rollback()
@ -1662,7 +1688,7 @@ def _db_configuration_update_helper():
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
_db_configuration_result(_(u"Database error: %(error)s.", error=e.orig), gdrive_error) _db_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig), gdrive_error)
try: try:
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db") metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
@ -1672,7 +1698,7 @@ def _db_configuration_update_helper():
return _db_configuration_result('{}'.format(ex), gdrive_error) return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured \ if db_change or not db_valid or not config.db_configured \
or config.config_calibre_dir != to_save["config_calibre_dir"]: or config.config_calibre_dir != to_save["config_calibre_dir"]:
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']: if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error) return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
else: else:
@ -1694,7 +1720,7 @@ def _db_configuration_update_helper():
_config_string(to_save, "config_calibre_dir") _config_string(to_save, "config_calibre_dir")
calibre_db.update_config(config) calibre_db.update_config(config)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning") flash(_("DB is not Writeable"), category="warning")
config.save() config.save()
return _db_configuration_result(None, gdrive_error) return _db_configuration_result(None, gdrive_error)
@ -1791,7 +1817,7 @@ def _configuration_update_helper():
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
_configuration_result(_(u"Database error: %(error)s.", error=e.orig)) _configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig))
config.save() config.save()
if reboot_required: if reboot_required:
@ -1807,7 +1833,7 @@ def _configuration_result(error_flash=None, reboot=False):
config.load() config.load()
resp['result'] = [{'type': "danger", 'message': error_flash}] resp['result'] = [{'type': "danger", 'message': error_flash}]
else: else:
resp['result'] = [{'type': "success", 'message': _(u"Calibre-Web configuration updated")}] resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
resp['reboot'] = reboot resp['reboot'] = reboot
resp['config_upload'] = config.config_upload_formats resp['config_upload'] = config.config_upload_formats
return Response(json.dumps(resp), mimetype='application/json') return Response(json.dumps(resp), mimetype='application/json')
@ -1838,7 +1864,7 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
gdriveError=gdrive_error, gdriveError=gdrive_error,
gdrivefolders=gdrivefolders, gdrivefolders=gdrivefolders,
feature_support=feature_support, feature_support=feature_support,
title=_(u"Database Configuration"), page="dbconfig") title=_("Database Configuration"), page="dbconfig")
def _handle_new_user(to_save, content, languages, translations, kobo_support): def _handle_new_user(to_save, content, languages, translations, kobo_support):
@ -1853,7 +1879,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
try: try:
if not to_save["name"] or not to_save["email"] or not to_save["password"]: if not to_save["name"] or not to_save["email"] or not to_save["password"]:
log.info("Missing entries on new user") log.info("Missing entries on new user")
raise Exception(_(u"Please fill out all fields!")) raise Exception(_("Oops! Please complete all fields."))
content.password = generate_password_hash(helper.valid_password(to_save.get("password", ""))) content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
content.email = check_email(to_save["email"]) content.email = check_email(to_save["email"])
# Query username, if not existing, change # Query username, if not existing, change
@ -1862,13 +1888,13 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.kindle_mail = valid_email(to_save["kindle_mail"]) content.kindle_mail = valid_email(to_save["kindle_mail"])
if config.config_public_reg and not check_valid_domain(content.email): if config.config_public_reg and not check_valid_domain(content.email):
log.info("E-mail: {} for new user is not from valid domain".format(content.email)) log.info("E-mail: {} for new user is not from valid domain".format(content.email))
raise Exception(_(u"E-mail is not from valid domain")) raise Exception(_("E-mail is not from valid domain"))
except Exception as ex: except Exception as ex:
flash(str(ex), category="error") flash(str(ex), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, return render_title_template("user_edit.html", new_user=1, content=content,
config=config, config=config,
translations=translations, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser", languages=languages, title=_("Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
try: try:
content.allowed_tags = config.config_allowed_tags content.allowed_tags = config.config_allowed_tags
@ -1879,17 +1905,17 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on" content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.name), category="success") flash(_("User '%(user)s' created", user=content.name), category="success")
log.debug("User {} created".format(content.name)) log.debug("User {} created".format(content.name))
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
log.error("Found an existing account for {} or {}".format(content.name, content.email)) log.error("Found an existing account for {} or {}".format(content.name, content.email))
flash(_("Found an existing account for this e-mail address or name."), category="error") flash(_("Oops! An account already exists for this Email. or name."), category="error")
except OperationalError as e: except OperationalError as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
def _delete_user(content): def _delete_user(content):
@ -1971,10 +1997,11 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
if to_save.get("locale"): if to_save.get("locale"):
content.locale = to_save["locale"] content.locale = to_save["locale"]
try: try:
if to_save.get('password', "") != "": new_email = valid_email(to_save.get("email", content.email))
content.password = generate_password_hash(helper.valid_password(to_save['password'])) if not new_email:
if to_save.get("email", content.email) != content.email: raise Exception(_("Email can't be empty and has to be a valid Email"))
content.email = check_email(to_save["email"]) if new_email != content.email:
content.email = check_email(new_email)
# Query username, 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":
@ -1994,19 +2021,19 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
content=content, content=content,
config=config, config=config,
registered_oauth=oauth_check, registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", nick=content.name), title=_("Edit User %(nick)s", nick=content.name),
page="edituser") page="edituser")
try: try:
ub.session_commit() ub.session_commit()
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success") flash(_("User '%(nick)s' updated", nick=content.name), category="success")
except IntegrityError as ex: except IntegrityError as ex:
ub.session.rollback() ub.session.rollback()
log.error("An unknown error occurred while changing user: {}".format(str(ex))) log.error("An unknown error occurred while changing user: {}".format(str(ex)))
flash(_(u"An unknown error occurred. Please try again later."), category="error") flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
except OperationalError as e: except OperationalError as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return "" return ""

@ -9,8 +9,6 @@ log = logger.create()
babel = Babel() babel = Babel()
@babel.localeselector
def get_locale(): def get_locale():
# if a user is logged in, use the locale from the user settings # if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None) user = getattr(g, 'user', None)

@ -29,7 +29,7 @@ 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 - unknown 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])

@ -138,7 +138,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
file_path=tmp_file_path, file_path=tmp_file_path,
extension=original_file_extension, extension=original_file_extension,
title=original_file_name, title=original_file_name,
author=u'Unknown', author='Unknown',
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable), cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
description="", description="",
tags="", tags="",

@ -74,12 +74,12 @@ class _Settings(_Base):
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='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|Le|La|Les|L\'|Un|Une)\s+')
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)

@ -163,7 +163,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher, pubdate, identifiers') 'series_id, languages, publisher, pubdate, identifiers')
STABLE_VERSION = {'version': '0.6.19 Beta'} STABLE_VERSION = {'version': '0.6.19'}
NIGHTLY_VERSION = dict() NIGHTLY_VERSION = dict()
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'

@ -111,66 +111,70 @@ class Identifiers(Base):
def format_type(self): def format_type(self):
format_type = self.type.lower() format_type = self.type.lower()
if format_type == 'amazon': if format_type == 'amazon':
return u"Amazon" return "Amazon"
elif format_type.startswith("amazon_"): elif format_type.startswith("amazon_"):
return u"Amazon.{0}".format(format_type[7:]) return "Amazon.{0}".format(format_type[7:])
elif format_type == "isbn": elif format_type == "isbn":
return u"ISBN" return "ISBN"
elif format_type == "doi": elif format_type == "doi":
return u"DOI" return "DOI"
elif format_type == "douban": elif format_type == "douban":
return u"Douban" return "Douban"
elif format_type == "goodreads": elif format_type == "goodreads":
return u"Goodreads" return "Goodreads"
elif format_type == "babelio": elif format_type == "babelio":
return u"Babelio" return "Babelio"
elif format_type == "google": elif format_type == "google":
return u"Google Books" return "Google Books"
elif format_type == "kobo": elif format_type == "kobo":
return u"Kobo" return "Kobo"
elif format_type == "litres": elif format_type == "litres":
return u"ЛитРес" return "ЛитРес"
elif format_type == "issn": elif format_type == "issn":
return u"ISSN" return "ISSN"
elif format_type == "isfdb": elif format_type == "isfdb":
return u"ISFDB" return "ISFDB"
if format_type == "lubimyczytac": if format_type == "lubimyczytac":
return u"Lubimyczytac" return "Lubimyczytac"
if format_type == "databazeknih":
return "Databáze knih"
else: else:
return self.type return self.type
def __repr__(self): def __repr__(self):
format_type = self.type.lower() format_type = self.type.lower()
if format_type == "amazon" or format_type == "asin": if format_type == "amazon" or format_type == "asin":
return u"https://amazon.com/dp/{0}".format(self.val) return "https://amazon.com/dp/{0}".format(self.val)
elif format_type.startswith('amazon_'): elif format_type.startswith('amazon_'):
return u"https://amazon.{0}/dp/{1}".format(format_type[7:], self.val) return "https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
elif format_type == "isbn": elif format_type == "isbn":
return u"https://www.worldcat.org/isbn/{0}".format(self.val) return "https://www.worldcat.org/isbn/{0}".format(self.val)
elif format_type == "doi": elif format_type == "doi":
return u"https://dx.doi.org/{0}".format(self.val) return "https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads": elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val) return "https://www.goodreads.com/book/show/{0}".format(self.val)
elif format_type == "babelio": elif format_type == "babelio":
return u"https://www.babelio.com/livres/titre/{0}".format(self.val) return "https://www.babelio.com/livres/titre/{0}".format(self.val)
elif format_type == "douban": elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val) return "https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google": elif format_type == "google":
return u"https://books.google.com/books?id={0}".format(self.val) return "https://books.google.com/books?id={0}".format(self.val)
elif format_type == "kobo": elif format_type == "kobo":
return u"https://www.kobo.com/ebook/{0}".format(self.val) return "https://www.kobo.com/ebook/{0}".format(self.val)
elif format_type == "lubimyczytac": elif format_type == "lubimyczytac":
return u"https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val) return "https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
elif format_type == "litres": elif format_type == "litres":
return u"https://www.litres.ru/{0}".format(self.val) return "https://www.litres.ru/{0}".format(self.val)
elif format_type == "issn": elif format_type == "issn":
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val) return "https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb": elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val) return "http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif format_type == "databazeknih":
return "https://www.databazeknih.cz/knihy/{0}".format(self.val)
elif self.val.lower().startswith("javascript:"): elif self.val.lower().startswith("javascript:"):
return quote(self.val) return quote(self.val)
else: else:
return u"{0}".format(self.val) return "{0}".format(self.val)
class Comments(Base): class Comments(Base):
@ -188,7 +192,7 @@ class Comments(Base):
return self.text return self.text
def __repr__(self): def __repr__(self):
return u"<Comments({0})>".format(self.text) return "<Comments({0})>".format(self.text)
class Tags(Base): class Tags(Base):
@ -204,7 +208,7 @@ class Tags(Base):
return self.name return self.name
def __repr__(self): def __repr__(self):
return u"<Tags('{0})>".format(self.name) return "<Tags('{0})>".format(self.name)
class Authors(Base): class Authors(Base):
@ -224,7 +228,7 @@ class Authors(Base):
return self.name return self.name
def __repr__(self): def __repr__(self):
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link) return "<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
class Series(Base): class Series(Base):
@ -242,7 +246,7 @@ class Series(Base):
return self.name return self.name
def __repr__(self): def __repr__(self):
return u"<Series('{0},{1}')>".format(self.name, self.sort) return "<Series('{0},{1}')>".format(self.name, self.sort)
class Ratings(Base): class Ratings(Base):
@ -258,7 +262,7 @@ class Ratings(Base):
return self.rating return self.rating
def __repr__(self): def __repr__(self):
return u"<Ratings('{0}')>".format(self.rating) return "<Ratings('{0}')>".format(self.rating)
class Languages(Base): class Languages(Base):
@ -277,7 +281,7 @@ class Languages(Base):
return self.lang_code return self.lang_code
def __repr__(self): def __repr__(self):
return u"<Languages('{0}')>".format(self.lang_code) return "<Languages('{0}')>".format(self.lang_code)
class Publishers(Base): class Publishers(Base):
@ -295,7 +299,7 @@ class Publishers(Base):
return self.name return self.name
def __repr__(self): def __repr__(self):
return u"<Publishers('{0},{1}')>".format(self.name, self.sort) return "<Publishers('{0},{1}')>".format(self.name, self.sort)
class Data(Base): class Data(Base):
@ -319,7 +323,16 @@ class Data(Base):
return self.name return self.name
def __repr__(self): def __repr__(self):
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name) return "<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
class Metadata_Dirtied(Base):
__tablename__ = 'metadata_dirtied'
id = Column(Integer, primary_key=True, autoincrement=True)
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
def __init__(self, book):
self.book = book
class Books(Base): class Books(Base):
@ -364,7 +377,7 @@ class Books(Base):
self.has_cover = (has_cover != None) self.has_cover = (has_cover != None)
def __repr__(self): def __repr__(self):
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort, return "<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
self.timestamp, self.pubdate, self.series_index, self.timestamp, self.pubdate, self.series_index,
self.last_modified, self.path, self.has_cover) self.last_modified, self.path, self.has_cover)
@ -390,6 +403,30 @@ class CustomColumns(Base):
display_dict = json.loads(self.display) display_dict = json.loads(self.display)
return display_dict return display_dict
def to_json(self, value, extra, sequence):
content = dict()
content['table'] = "custom_column_" + str(self.id)
content['column'] = "value"
content['datatype'] = self.datatype
content['is_multiple'] = None if not self.is_multiple else self.is_multiple
content['kind'] = "field"
content['name'] = self.name
content['search_terms'] = ['#' + self.label]
content['label'] = self.label
content['colnum'] = self.id
content['display'] = self.get_display_dict()
content['is_custom'] = True
content['is_category'] = self.datatype in ['text', 'rating', 'enumeration', 'series']
content['link_column'] = "value"
content['category_sort'] = "value"
content['is_csp'] = False
content['is_editable'] = self.editable
content['rec_index'] = sequence + 22 # toDo why ??
content['#value#'] = value
content['#extra#'] = extra
content['is_multiple2'] = {}
return json.dumps(content, ensure_ascii=False)
class AlchemyEncoder(json.JSONEncoder): class AlchemyEncoder(json.JSONEncoder):
@ -641,6 +678,18 @@ class CalibreDB:
def get_book_format(self, book_id, file_format): def get_book_format(self, book_id, file_format):
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first() return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
def set_metadata_dirty(self, book_id):
if not self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).one_or_none():
self.session.add(Metadata_Dirtied(book_id))
def delete_dirty_metadata(self, book_id):
try:
self.session.query(Metadata_Dirtied).filter(Metadata_Dirtied.book == book_id).delete()
self.session.commit()
except (OperationalError) as e:
self.session.rollback()
log.error("Database error: {}".format(e))
# Language and content filters for displaying in the UI # Language and content filters for displaying in the UI
def common_filters(self, allow_show_archived=False, return_all_languages=False): def common_filters(self, allow_show_archived=False, return_all_languages=False):
if not allow_show_archived: if not allow_show_archived:

@ -38,7 +38,7 @@ from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from flask_babel import get_locale from flask_babel import get_locale
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.orm.exc import StaleDataError from sqlalchemy.orm.exc import StaleDataError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
@ -107,7 +107,7 @@ def edit_book(book_id):
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
# Book not found # Book not found
if not book: if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error") category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
@ -151,7 +151,7 @@ def edit_book(book_id):
if to_save.get("cover_url", None): if to_save.get("cover_url", None):
if not current_user.role_upload(): if not current_user.role_upload():
edit_error = True edit_error = True
flash(_(u"User has no rights to upload cover"), category="error") flash(_("User has no rights to upload cover"), category="error")
if to_save["cover_url"].endswith('/static/generic_cover.jpg'): if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
book.has_cover = 0 book.has_cover = 0
else: else:
@ -203,6 +203,7 @@ def edit_book(book_id):
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id, all=True) kobo_sync_status.remove_synced_book(edited_books_id, all=True)
calibre_db.set_metadata_dirty(book.id)
calibre_db.session.merge(book) calibre_db.session.merge(book)
calibre_db.session.commit() calibre_db.session.commit()
@ -222,10 +223,10 @@ def edit_book(book_id):
calibre_db.session.rollback() calibre_db.session.rollback()
flash(str(e), category="error") flash(str(e), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
calibre_db.session.rollback() calibre_db.session.rollback()
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))
except Exception as ex: except Exception as ex:
log.error_or_exception(ex) log.error_or_exception(ex)
@ -277,6 +278,8 @@ def upload():
move_coverfile(meta, db_book) move_coverfile(meta, db_book)
if modify_date:
calibre_db.set_metadata_dirty(book_id)
# save data to database, reread data # save data to database, reread data
calibre_db.session.commit() calibre_db.session.commit()
@ -285,7 +288,7 @@ def upload():
if error: if error:
flash(error, category="error") flash(error, category="error")
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title)) link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
upload_text = N_(u"File %(file)s uploaded", file=link) upload_text = N_("File %(file)s uploaded", file=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title))) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
helper.add_book_to_thumbnail_cache(book_id) helper.add_book_to_thumbnail_cache(book_id)
@ -299,7 +302,7 @@ def upload():
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
@ -312,7 +315,7 @@ def convert_bookformat(book_id):
book_format_to = request.form.get('book_format_to', None) book_format_to = request.form.get('book_format_to', None)
if (book_format_from is None) or (book_format_to is None): if (book_format_from is None) or (book_format_to is None):
flash(_(u"Source or destination format for conversion missing"), category="error") flash(_("Source or destination format for conversion missing"), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
@ -320,11 +323,11 @@ def convert_bookformat(book_id):
book_format_to.upper(), current_user.name) book_format_to.upper(), current_user.name)
if rtn is None: if rtn is None:
flash(_(u"Book successfully queued for converting to %(book_format)s", flash(_("Book successfully queued for converting to %(book_format)s",
book_format=book_format_to), book_format=book_format_to),
category="success") category="success")
else: else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") flash(_("There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
@ -555,6 +558,7 @@ def table_xchange_author_title():
renamed_author=renamed) renamed_author=renamed)
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
calibre_db.set_metadata_dirty(book.id)
try: try:
calibre_db.session.commit() calibre_db.session.commit()
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
@ -569,9 +573,9 @@ def table_xchange_author_title():
def merge_metadata(to_save, meta): def merge_metadata(to_save, meta):
if to_save.get('author_name', "") == _(u'Unknown'): if to_save.get('author_name', "") == _('Unknown'):
to_save['author_name'] = '' to_save['author_name'] = ''
if to_save.get('book_title', "") == _(u'Unknown'): if to_save.get('book_title', "") == _('Unknown'):
to_save['book_title'] = '' to_save['book_title'] = ''
for s_field, m_field in [ for s_field, m_field in [
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'), ('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
@ -607,7 +611,7 @@ def prepare_authors(authr):
# we have all author names now # we have all author names now
if input_authors == ['']: if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author input_authors = [_('Unknown')] # prevent empty Author
renamed = list() renamed = list()
for in_aut in input_authors: for in_aut in input_authors:
@ -624,11 +628,11 @@ def prepare_authors(authr):
def prepare_authors_on_upload(title, authr): def prepare_authors_on_upload(title, authr):
if title != _(u'Unknown') and authr != _(u'Unknown'): if title != _('Unknown') and authr != _('Unknown'):
entry = calibre_db.check_exists_book(authr, title) entry = calibre_db.check_exists_book(authr, title)
if entry: if entry:
log.info("Uploaded book probably exists in library") log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ") flash(_("Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning") + Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
input_authors, renamed = prepare_authors(authr) input_authors, renamed = prepare_authors(authr)
@ -683,7 +687,7 @@ def create_book_on_upload(modify_date, meta):
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid) modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
if invalid: if invalid:
for lang in invalid: for lang in invalid:
flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning") flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning")
# handle tags # handle tags
modify_date |= edit_book_tags(meta.tags, db_book) modify_date |= edit_book_tags(meta.tags, db_book)
@ -733,7 +737,7 @@ def file_handling_on_upload(requested_file):
meta = uploader.upload(requested_file, config.config_rarfile_location) meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError): except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename) log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir", flash(_("File %(filename)s could not saved to temp dir",
filename=requested_file.filename), category="error") filename=requested_file.filename), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return meta, None return meta, None
@ -753,7 +757,7 @@ def move_coverfile(meta, db_book):
os.unlink(meta.cover) os.unlink(meta.cover)
except OSError as e: except OSError as e:
log.error("Failed to move cover file %s: %s", new_cover_path, e) log.error("Failed to move cover file %s: %s", new_cover_path, e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path, flash(_("Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
error=e), error=e),
category="error") category="error")
@ -767,7 +771,7 @@ def delete_whole_book(book_id, book):
# check if only this book links to: # check if only this book links to:
# author, language, series, tags, custom columns # author, language, series, tags, custom columns
modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author') modify_database_object([''], book.authors, db.Authors, calibre_db.session, 'author')
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags') modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series') modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages') modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
@ -888,7 +892,7 @@ def render_edit_book(book_id):
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all() cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
if not book: if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error") category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
@ -923,7 +927,7 @@ def render_edit_book(book_id):
if kepub_possible: if kepub_possible:
allowed_conversion_formats.append('kepub') allowed_conversion_formats.append('kepub')
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
title=_(u"edit metadata"), page="editbook", title=_("edit metadata"), page="editbook",
conversion_formats=allowed_conversion_formats, conversion_formats=allowed_conversion_formats,
config=config, config=config,
source_formats=valid_source_formats) source_formats=valid_source_formats)
@ -1008,7 +1012,7 @@ def edit_book_languages(languages, book, upload_mode=False, invalid=None):
if isinstance(invalid, list): if isinstance(invalid, list):
invalid.append(lang) invalid.append(lang)
else: else:
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=lang)) raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
# ToDo: Not working correct # ToDo: Not working correct
if upload_mode and len(input_l) == 1: if upload_mode and len(input_l) == 1:
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view # If the language of the file is excluded from the users view, it's not imported, to allow the user to view
@ -1150,7 +1154,7 @@ def upload_single_file(file_request, book, book_id):
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
flash(_(u"User has no rights to upload additional file formats"), category="error") flash(_("User has no rights to upload additional file formats"), category="error")
return False return False
if '.' in requested_file.filename: if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
@ -1171,12 +1175,12 @@ def upload_single_file(file_request, book, book_id):
try: try:
os.makedirs(filepath) os.makedirs(filepath)
except OSError: except OSError:
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
return False return False
try: try:
requested_file.save(saved_filename) requested_file.save(saved_filename)
except OSError: except OSError:
flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") flash(_("Failed to store file %(file)s.", file=saved_filename), category="error")
return False return False
file_size = os.path.getsize(saved_filename) file_size = os.path.getsize(saved_filename)
@ -1194,12 +1198,12 @@ def upload_single_file(file_request, book, book_id):
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return False # return redirect(url_for('web.show_book', book_id=book.id)) return False # return redirect(url_for('web.show_book', book_id=book.id))
# Queue uploader info # Queue uploader info
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))
upload_text = N_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link) upload_text = N_("File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title))) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
return uploader.process( return uploader.process(
@ -1214,7 +1218,7 @@ def upload_cover(cover_request, book):
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
flash(_(u"User has no rights to upload cover"), category="error") flash(_("User has no rights to upload cover"), category="error")
return False return False
ret, message = helper.save_cover(requested_file, book.path) ret, message = helper.save_cover(requested_file, book.path)
if ret is True: if ret is True:

@ -80,13 +80,13 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
if epub_metadata['subject'] == 'Unknown': if epub_metadata['subject'] == 'Unknown':
epub_metadata['subject'] = '' epub_metadata['subject'] = ''
if epub_metadata['publisher'] == u'Unknown': if epub_metadata['publisher'] == 'Unknown':
epub_metadata['publisher'] = '' epub_metadata['publisher'] = ''
if epub_metadata['date'] == u'Unknown': if epub_metadata['date'] == 'Unknown':
epub_metadata['date'] = '' epub_metadata['date'] = ''
if epub_metadata['description'] == u'Unknown': if epub_metadata['description'] == 'Unknown':
description = tree.xpath("//*[local-name() = 'description']/text()") description = tree.xpath("//*[local-name() = 'description']/text()")
if len(description) > 0: if len(description) > 0:
epub_metadata['description'] = description epub_metadata['description'] = description
@ -102,11 +102,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
identifiers = [] identifiers = []
for node in p.xpath('dc:identifier', namespaces=ns): for node in p.xpath('dc:identifier', namespaces=ns):
identifier_name=node.attrib.values()[-1]; try:
identifier_value=node.text; identifier_name = node.attrib.values()[-1]
if identifier_name in ('uuid','calibre'): except IndexError:
continue; continue
identifiers.append( [identifier_name, identifier_value] ) identifier_value = node.text
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
continue
identifiers.append([identifier_name, identifier_value])
if not epub_metadata['title']: if not epub_metadata['title']:
title = original_file_name title = original_file_name

@ -38,19 +38,19 @@ def get_fb2_info(tmp_file_path, original_file_extension):
if len(last_name): if len(last_name):
last_name = last_name[0] last_name = last_name[0]
else: else:
last_name = u'' last_name = ''
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns) middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
if len(middle_name): if len(middle_name):
middle_name = middle_name[0] middle_name = middle_name[0]
else: else:
middle_name = u'' middle_name = ''
first_name = element.xpath('fb:first-name/text()', namespaces=ns) first_name = element.xpath('fb:first-name/text()', namespaces=ns)
if len(first_name): if len(first_name):
first_name = first_name[0] first_name = first_name[0]
else: else:
first_name = u'' first_name = ''
return (first_name + u' ' return (first_name + ' '
+ middle_name + u' ' + middle_name + ' '
+ last_name) + last_name)
author = str(", ".join(map(get_author, authors))) author = str(", ".join(map(get_author, authors)))
@ -59,12 +59,12 @@ def get_fb2_info(tmp_file_path, original_file_extension):
if len(title): if len(title):
title = str(title[0]) title = str(title[0])
else: else:
title = u'' title = ''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns) description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description): if len(description):
description = str(description[0]) description = str(description[0])
else: else:
description = u'' description = ''
return BookMeta( return BookMeta(
file_path=tmp_file_path, file_path=tmp_file_path,

@ -55,7 +55,7 @@ def authenticate_google_drive():
try: try:
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl() authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
except gdriveutils.InvalidConfigError: except gdriveutils.InvalidConfigError:
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'), flash(_('Google Drive setup not completed, try to deactivate and activate Google Drive again'),
category="error") category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return redirect(authUrl) return redirect(authUrl)
@ -91,9 +91,9 @@ def watch_gdrive():
config.save() config.save()
except HttpError as e: except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0] reason=json.loads(e.content)['error']['errors'][0]
if reason['reason'] == u'push.webhookUrlUnauthorized': if reason['reason'] == 'push.webhookUrlUnauthorized':
flash(_(u'Callback domain is not verified, ' flash(_('Callback domain is not verified, '
u'please follow steps to verify domain in google developer console'), category="error") 'please follow steps to verify domain in google developer console'), category="error")
else: else:
flash(reason['message'], category="error") flash(reason['message'], category="error")

@ -556,7 +556,7 @@ def updateGdriveCalibreFromLocal():
# update gdrive.db on edit of books title # update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath): def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/' sqlCheckPath = newPath if newPath[-1] == '/' else newPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first() storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName: if storedPathName:
storedPathName.path = sqlCheckPath storedPathName.path = sqlCheckPath
@ -578,6 +578,7 @@ def deleteDatabaseEntry(ID):
# Gets cover file from gdrive # Gets cover file from gdrive
# ToDo: Check is this right everyone get read permissions on cover files?
def get_cover_via_gdrive(cover_path): def get_cover_via_gdrive(cover_path):
df = getFileFromEbooksFolder(cover_path, 'cover.jpg') df = getFileFromEbooksFolder(cover_path, 'cover.jpg')
if df: if df:
@ -600,6 +601,29 @@ def get_cover_via_gdrive(cover_path):
else: else:
return None return None
# Gets cover file from gdrive
def get_metadata_backup_via_gdrive(metadata_path):
df = getFileFromEbooksFolder(metadata_path, 'metadata.opf')
if df:
if not session.query(PermissionAdded).filter(PermissionAdded.gdrive_id == df['id']).first():
df.GetPermissions()
df.InsertPermission({
'type': 'anyone',
'value': 'anyone',
'role': 'writer', # ToDo needs write access
'withLink': True})
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return df.metadata.get('webContentLink')
else:
return None
# Creates chunks for downloading big files # Creates chunks for downloading big files
def partial(total_byte_len, part_size_limit): def partial(total_byte_len, part_size_limit):
s = [] s = []

@ -31,6 +31,7 @@ import unidecode
from flask import send_from_directory, make_response, redirect, abort, url_for from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, or_, text, func from sqlalchemy.sql.expression import true, false, and_, or_, text, func
from sqlalchemy.exc import InvalidRequestError, OperationalError from sqlalchemy.exc import InvalidRequestError, OperationalError
@ -57,6 +58,7 @@ from .subproc_wrapper import process_wait
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks.mail import TaskEmail from .tasks.mail import TaskEmail
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
from .tasks.metadata_backup import TaskBackupMetadata
log = logger.create() log = logger.create()
@ -74,30 +76,30 @@ except (ImportError, RuntimeError) as e:
def convert_book_format(book_id, calibre_path, old_book_format, new_book_format, user_id, ereader_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)
if not data: if not data:
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id) error_message = _("%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
log.error("convert_book_format: %s", error_message) log.error("convert_book_format: %s", error_message)
return error_message return error_message
file_path = os.path.join(calibre_path, book.path, data.name)
if config.config_use_google_drive: if config.config_use_google_drive:
if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()): if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
error_message = _(u"%(format)s not found on Google Drive: %(fn)s", error_message = _("%(format)s not found on Google Drive: %(fn)s",
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
else: else:
if not os.path.exists(file_path + "." + old_book_format.lower()): if not os.path.exists(file_path + "." + old_book_format.lower()):
error_message = _(u"%(format)s not found: %(fn)s", error_message = _("%(format)s not found: %(fn)s",
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 ereader_mail: if ereader_mail:
settings = config.get_mail_settings() settings = config.get_mail_settings()
settings['subject'] = _('Send to E-Reader') # pretranslate Subject for e-mail settings['subject'] = _('Send to eReader') # pretranslate Subject for Email
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.') settings['body'] = _('This Email has been sent via Calibre-Web.')
else: else:
settings = dict() settings = dict()
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
txt = u"{} -> {}: {}".format( txt = "{} -> {}: {}".format(
old_book_format.upper(), old_book_format.upper(),
new_book_format.upper(), new_book_format.upper(),
link) link)
@ -109,30 +111,30 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
# 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(ereader_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(_('Calibre-Web Test Email'), None, None,
config.get_mail_settings(), ereader_mail, N_(u"Test e-mail"), config.get_mail_settings(), ereader_mail, N_("Test Email"),
_(u'This e-mail has been sent via Calibre-Web.'))) _('This Email has been sent via Calibre-Web.')))
return return
# Send registration email or password reset email, depending on parameter resend (False means welcome email) # Send registration email or password reset email, depending on parameter resend (False means welcome email)
def send_registration_mail(e_mail, user_name, default_password, resend=False): def send_registration_mail(e_mail, user_name, default_password, resend=False):
txt = "Hello %s!\r\n" % user_name txt = "Hi %s!\r\n" % user_name
if not resend: if not resend:
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" txt += "Your account at Calibre-Web has been created.\r\n"
txt += "Please log in to your account using the following informations:\r\n" txt += "Please log in using the following information:\r\n"
txt += "User name: %s\r\n" % user_name txt += "Username: %s\r\n" % user_name
txt += "Password: %s\r\n" % default_password txt += "Password: %s\r\n" % default_password
txt += "Don't forget to change your password after first login.\r\n" txt += "Don't forget to change your password after your first login.\r\n"
txt += "Sincerely\r\n\r\n" txt += "Regards,\r\n\r\n"
txt += "Your Calibre-Web team" txt += "Calibre-Web"
WorkerThread.add(None, TaskEmail( WorkerThread.add(None, TaskEmail(
subject=_(u'Get Started with Calibre-Web'), subject=_('Get Started with Calibre-Web'),
filepath=None, filepath=None,
attachment=None, attachment=None,
settings=config.get_mail_settings(), settings=config.get_mail_settings(),
recipient=e_mail, recipient=e_mail,
task_message=N_(u"Registration e-mail for user: %(name)s", name=user_name), task_message=N_("Registration Email for user: %(name)s", name=user_name),
text=txt text=txt
)) ))
return return
@ -143,13 +145,13 @@ def check_send_to_ereader_with_converter(formats):
if 'MOBI' in formats and 'EPUB' not in formats: if 'MOBI' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub', book_formats.append({'format': 'Epub',
'convert': 1, 'convert': 1,
'text': _('Convert %(orig)s to %(format)s and send to E-Reader', 'text': _('Convert %(orig)s to %(format)s and send to eReader',
orig='Mobi', orig='Mobi',
format='Epub')}) format='Epub')})
if 'AZW3' in formats and 'EPUB' not in formats: if 'AZW3' in formats and 'EPUB' not in formats:
book_formats.append({'format': 'Epub', book_formats.append({'format': 'Epub',
'convert': 2, 'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to E-Reader', 'text': _('Convert %(orig)s to %(format)s and send to eReader',
orig='Azw3', orig='Azw3',
format='Epub')}) format='Epub')})
return book_formats return book_formats
@ -157,7 +159,7 @@ def check_send_to_ereader_with_converter(formats):
def check_send_to_ereader(entry): def check_send_to_ereader(entry):
""" """
returns all available book formats for sending to E-Reader returns all available book formats for sending to eReader
""" """
formats = list() formats = list()
book_formats = list() book_formats = list()
@ -168,24 +170,24 @@ def check_send_to_ereader(entry):
if 'EPUB' in formats: if 'EPUB' in formats:
book_formats.append({'format': 'Epub', book_formats.append({'format': 'Epub',
'convert': 0, 'convert': 0,
'text': _('Send %(format)s to E-Reader', format='Epub')}) 'text': _('Send %(format)s to eReader', 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 E-Reader', format='Mobi')}) 'text': _('Send %(format)s to eReader', 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 E-Reader', format='Pdf')}) 'text': _('Send %(format)s to eReader', 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 E-Reader', format='Azw')}) 'text': _('Send %(format)s to eReader', format='Azw')})
if config.config_converterpath: if config.config_converterpath:
book_formats.extend(check_send_to_ereader_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('Cannot find book entry %d', entry.id)
return None return None
@ -202,30 +204,30 @@ 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 E-Reader email, # 1: If Mobi file is existing, it's directly send to eReader email,
# 2: If Epub file is existing, it's converted and send to E-Reader email, # 2: If Epub file is existing, it's converted and send to eReader email,
# 3: If Pdf file is existing, it's directly send to E-Reader email # 3: If Pdf file is existing, it's directly send to eReader email
def send_mail(book_id, book_format, convert, ereader_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, ereader_mail) return convert_book_format(book_id, calibrepath, '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, ereader_mail) return convert_book_format(book_id, calibrepath, '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 E-Reader", book=link) email_text = N_("%(book)s send to eReader", book=link)
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name, WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
config.get_mail_settings(), ereader_mail, config.get_mail_settings(), ereader_mail,
email_text, _(u'This e-mail has been sent via Calibre-Web.'))) email_text, _('This Email has been sent via Calibre-Web.')))
return return
return _(u"The requested file could not be read. Maybe wrong permissions?") return _("The requested file could not be read. Maybe wrong permissions?")
def get_valid_filename(value, replace_whitespace=True, chars=128): def get_valid_filename(value, replace_whitespace=True, chars=128):
@ -233,16 +235,16 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
Returns the given string converted to a string that can be used for a clean Returns the given string converted to a string that can be used for a clean
filename. Limits num characters to 128 max. filename. Limits num characters to 128 max.
""" """
if value[-1:] == u'.': if value[-1:] == '.':
value = value[:-1]+u'_' value = value[:-1]+'_'
value = value.replace("/", "_").replace(":", "_").strip('\0') value = value.replace("/", "_").replace(":", "_").strip('\0')
if config.config_unicode_filename: if config.config_unicode_filename:
value = (unidecode.unidecode(value)) value = (unidecode.unidecode(value))
if replace_whitespace: if replace_whitespace:
# *+:\"/<>? are replaced by _ # *+:\"/<>? are replaced by _
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U) value = re.sub(r'[*+:\\\"/<>?]+', '_', value, flags=re.U)
# 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'[|]+', ',', value, flags=re.U)
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip() value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
@ -339,7 +341,7 @@ def edit_book_read_status(book_id, read_status=None):
return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column) return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column)
except (OperationalError, InvalidRequestError) as ex: except (OperationalError, InvalidRequestError) as ex:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error(u"Read status could not set: {}".format(ex)) log.error("Read status could not set: {}".format(ex))
return _("Read status could not set: {}".format(ex.orig)) return _("Read status could not set: {}".format(ex.orig))
return "" return ""
@ -414,8 +416,8 @@ def clean_author_database(renamed_author, calibre_path="", local_book=None, gdri
g_file = gd.getFileFromEbooksFolder(all_new_path, g_file = gd.getFileFromEbooksFolder(all_new_path,
file_format.name + '.' + file_format.format.lower()) file_format.name + '.' + file_format.format.lower())
if g_file: if g_file:
gd.moveGdriveFileRemote(g_file, all_new_name + u'.' + file_format.format.lower()) gd.moveGdriveFileRemote(g_file, all_new_name + '.' + file_format.format.lower())
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + u'.' + file_format.format.lower()) gd.updateDatabaseOnEdit(g_file['id'], all_new_name + '.' + file_format.format.lower())
else: else:
log.error("File {} not found on gdrive" log.error("File {} not found on gdrive"
.format(all_new_path, file_format.name + '.' + file_format.format.lower())) .format(all_new_path, file_format.name + '.' + file_format.format.lower()))
@ -508,25 +510,25 @@ def update_dir_structure_gdrive(book_id, first_author, renamed_author):
authordir = book.path.split('/')[0] authordir = book.path.split('/')[0]
titledir = book.path.split('/')[1] titledir = book.path.split('/')[1]
new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True) new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True)
new_titledir = get_valid_filename(book.title, chars=96) + u" (" + str(book_id) + u")" new_titledir = get_valid_filename(book.title, chars=96) + " (" + str(book_id) + ")"
if titledir != new_titledir: if titledir != new_titledir:
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if g_file: if g_file:
gd.moveGdriveFileRemote(g_file, new_titledir) gd.moveGdriveFileRemote(g_file, new_titledir)
book.path = book.path.split('/')[0] + u'/' + new_titledir book.path = book.path.split('/')[0] + '/' + new_titledir
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
else: else:
return _(u'File %(file)s not found on Google Drive', file=book.path) # file not found return _('File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir and authordir not in renamed_author: if authordir != new_authordir and authordir not in renamed_author:
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir) g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if g_file: if g_file:
gd.moveGdriveFolderRemote(g_file, new_authordir) gd.moveGdriveFolderRemote(g_file, new_authordir)
book.path = new_authordir + u'/' + book.path.split('/')[1] book.path = new_authordir + '/' + book.path.split('/')[1]
gd.updateDatabaseOnEdit(g_file['id'], book.path) gd.updateDatabaseOnEdit(g_file['id'], book.path)
else: else:
return _(u'File %(file)s not found on Google Drive', file=authordir) # file not found return _('File %(file)s not found on Google Drive', file=authordir) # file not found
# change location in database to new author/title path # change location in database to new author/title path
book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/') book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
@ -598,7 +600,7 @@ def delete_book_gdrive(book, book_format):
gd.deleteDatabaseEntry(g_file['id']) gd.deleteDatabaseEntry(g_file['id'])
g_file.Trash() g_file.Trash()
else: else:
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found error = _('Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error is None, error return error is None, error
@ -638,26 +640,28 @@ def uniq(inpt):
def check_email(email): def check_email(email):
email = valid_email(email) email = valid_email(email)
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first(): if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
log.error(u"Found an existing account for this e-mail address") log.error("Found an existing account for this Email address")
raise Exception(_(u"Found an existing account for this e-mail address")) raise Exception(_("Found an existing account for this Email address"))
return email return email
def check_username(username): def check_username(username):
username = username.strip() username = username.strip()
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar(): if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
log.error(u"This username is already taken") log.error("This username is already taken")
raise Exception(_(u"This username is already taken")) raise Exception(_("This username is already taken"))
return username return username
def valid_email(email): def valid_email(email):
email = email.strip() email = email.strip()
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation # if email is not deleted
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$", if email:
email): # Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
log.error(u"Invalid e-mail address format") if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
raise Exception(_(u"Invalid e-mail address format")) email):
log.error("Invalid Email address format")
raise Exception(_("Invalid Email address format"))
return email return email
def valid_password(check_password): def valid_password(check_password):
@ -699,7 +703,8 @@ def update_dir_structure(book_id,
def delete_book(book, calibrepath, book_format): def delete_book(book, calibrepath, book_format):
if not book_format: if not book_format:
clear_cover_thumbnail_cache(book.id) ## here it breaks clear_cover_thumbnail_cache(book.id) ## here it breaks
calibre_db.delete_dirty_metadata(book.id)
if config.config_use_google_drive: if config.config_use_google_drive:
return delete_book_gdrive(book, book_format) return delete_book_gdrive(book, book_format)
else: else:
@ -849,8 +854,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
try: try:
os.makedirs(filepath) os.makedirs(filepath)
except OSError: except OSError:
log.error(u"Failed to create path for cover") log.error("Failed to create path for cover")
return False, _(u"Failed to create path for cover") return False, _("Failed to create path for cover")
try: try:
# upload of jgp file without wand # upload of jgp file without wand
if isinstance(img, requests.Response): if isinstance(img, requests.Response):
@ -865,8 +870,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
# upload of jpg/png... from hdd # upload of jpg/png... from hdd
img.save(os.path.join(filepath, saved_filename)) img.save(os.path.join(filepath, saved_filename))
except (IOError, OSError): except (IOError, OSError):
log.error(u"Cover-file is not a valid image file, or could not be stored") log.error("Cover-file is not a valid image file, or could not be stored")
return False, _(u"Cover-file is not a valid image file, or could not be stored") return False, _("Cover-file is not a valid image file, or could not be stored")
return True, None return True, None
@ -956,7 +961,7 @@ def check_unrar(unrar_location):
except (OSError, UnicodeDecodeError) as err: except (OSError, UnicodeDecodeError) as err:
log.error_or_exception(err) log.error_or_exception(err)
return _('Error excecuting UnRar') return _('Error executing UnRar')
def json_serial(obj): def json_serial(obj):
@ -1045,3 +1050,11 @@ def add_book_to_thumbnail_cache(book_id):
def update_thumbnail_cache(): def update_thumbnail_cache():
if config.schedule_generate_book_covers: if config.schedule_generate_book_covers:
WorkerThread.add(None, TaskGenerateCoverThumbnails()) WorkerThread.add(None, TaskGenerateCoverThumbnails())
def set_all_metadata_dirty():
WorkerThread.add(None, TaskBackupMetadata(export_language=get_locale(),
translated_title=_("Cover"),
set_dirty=True,
task_message=N_("Queue all books for metadata backup")),
hidden=False)

File diff suppressed because it is too large Load Diff

@ -45,6 +45,7 @@ import requests
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
from . import isoLanguages
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
from .helper import get_download_link from .helper import get_download_link
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
@ -155,7 +156,7 @@ def HandleSyncRequest():
new_archived_last_modified = datetime.datetime.min new_archived_last_modified = datetime.datetime.min
sync_results = [] sync_results = []
# We reload the book database so that the user get's a fresh view of the library # We reload the book database so that the user gets a fresh view of the library
# in case of external changes (e.g: adding a book through Calibre). # in case of external changes (e.g: adding a book through Calibre).
calibre_db.reconnect_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
@ -355,7 +356,7 @@ def HandleMetadataRequest(book_uuid):
log.info("Kobo library metadata request received for book %s" % book_uuid) log.info("Kobo library metadata request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data: if not book or not book.data:
log.info(u"Book %s not found in database", book_uuid) log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request() return redirect_or_proxy_request()
metadata = get_metadata(book) metadata = get_metadata(book)
@ -443,6 +444,12 @@ def get_seriesindex(book):
return book.series_index or 1 return book.series_index or 1
def get_language(book):
if not book.languages:
return 'en'
return isoLanguages.get(part3=book.languages[0].lang_code).part1
def get_metadata(book): def get_metadata(book):
download_urls = [] download_urls = []
kepub = [data for data in book.data if data.format == 'KEPUB'] kepub = [data for data in book.data if data.format == 'KEPUB']
@ -480,7 +487,7 @@ def get_metadata(book):
"IsInternetArchive": False, "IsInternetArchive": False,
"IsPreOrder": False, "IsPreOrder": False,
"IsSocialEnabled": True, "IsSocialEnabled": True,
"Language": "en", "Language": get_language(book),
"PhoneticPronunciations": {}, "PhoneticPronunciations": {},
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate), "PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
"Publisher": {"Imprint": "", "Name": get_publisher(book), }, "Publisher": {"Imprint": "", "Name": get_publisher(book), },
@ -508,7 +515,7 @@ def get_metadata(book):
@requires_kobo_auth @requires_kobo_auth
# Creates a Shelf with the given items, and returns the shelf's uuid. # Creates a Shelf with the given items, and returns the shelf's uuid.
def HandleTagCreate(): def HandleTagCreate():
# catch delete requests, otherwise the are handeld in the book delete handler # catch delete requests, otherwise the are handled in the book delete handler
if request.method == "DELETE": if request.method == "DELETE":
abort(405) abort(405)
name, items = None, None name, items = None, None
@ -752,7 +759,7 @@ def create_kobo_tag(shelf):
for book_shelf in shelf.books: for book_shelf in shelf.books:
book = calibre_db.get_book(book_shelf.book_id) book = calibre_db.get_book(book_shelf.book_id)
if not book: if not book:
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id) log.info("Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
continue continue
tag["Items"].append( tag["Items"].append(
{ {
@ -769,7 +776,7 @@ def create_kobo_tag(shelf):
def HandleStateRequest(book_uuid): def HandleStateRequest(book_uuid):
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data: if not book or not book.data:
log.info(u"Book %s not found in database", book_uuid) log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request() return redirect_or_proxy_request()
kobo_reading_state = get_or_create_reading_state(book.id) kobo_reading_state = get_or_create_reading_state(book.id)
@ -944,7 +951,7 @@ def HandleBookDeletionRequest(book_uuid):
log.info("Kobo book delete request received for book %s" % book_uuid) log.info("Kobo book delete request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book: if not book:
log.info(u"Book %s not found in database", book_uuid) log.info("Book %s not found in database", book_uuid)
return redirect_or_proxy_request() return redirect_or_proxy_request()
book_id = book.id book_id = book.id
@ -958,7 +965,7 @@ def HandleBookDeletionRequest(book_uuid):
@csrf.exempt @csrf.exempt
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"]) @kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
def HandleUnimplementedRequest(dummy=None): def HandleUnimplementedRequest(dummy=None):
log.debug("Unimplemented Library Request received: %s", request.base_url) log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@ -970,7 +977,7 @@ def HandleUnimplementedRequest(dummy=None):
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"]) @kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"]) @kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
def HandleUserRequest(dummy=None): def HandleUserRequest(dummy=None):
log.debug("Unimplemented User Request received: %s", request.base_url) log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@ -1010,7 +1017,7 @@ def handle_getests():
@kobo.route("/v1/affiliate", methods=["GET", "POST"]) @kobo.route("/v1/affiliate", methods=["GET", "POST"])
@kobo.route("/v1/deals", methods=["GET", "POST"]) @kobo.route("/v1/deals", methods=["GET", "POST"])
def HandleProductsRequest(dummy=None): def HandleProductsRequest(dummy=None):
log.debug("Unimplemented Products Request received: %s", request.base_url) log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()

@ -113,7 +113,7 @@ def generate_auth_token(user_id):
return render_title_template( return render_title_template(
"generate_kobo_auth_url.html", "generate_kobo_auth_url.html",
title=_(u"Kobo Setup"), title=_("Kobo Setup"),
auth_token=auth_token.auth_token, auth_token=auth_token.auth_token,
warning = warning warning = warning
) )

@ -63,11 +63,11 @@ class Amazon(Metadata):
r.raise_for_status() r.raise_for_status()
except Exception as ex: except Exception as ex:
log.warning(ex) log.warning(ex)
return return None
long_soup = BS(r.text, "lxml") #~4sec :/ long_soup = BS(r.text, "lxml") #~4sec :/
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"}) soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
if soup2 is None: if soup2 is None:
return return None
try: try:
match = MetaRecord( match = MetaRecord(
title = "", title = "",
@ -115,7 +115,7 @@ class Amazon(Metadata):
return match, index return match, index
except Exception as e: except Exception as e:
log.error_or_exception(e) log.error_or_exception(e)
return return None
val = list() val = list()
if self.active: if self.active:
@ -127,10 +127,10 @@ class Amazon(Metadata):
results.raise_for_status() results.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
log.error_or_exception(e) log.error_or_exception(e)
return None return []
except Exception as e: except Exception as e:
log.warning(e) log.warning(e)
return None return []
soup = BS(results.text, 'html.parser') soup = BS(results.text, 'html.parser')
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
soup.findAll("div", attrs={"data-component-type": "s-search-result"})] soup.findAll("div", attrs={"data-component-type": "s-search-result"})]

@ -43,7 +43,8 @@ class Douban(Metadata):
__id__ = "douban" __id__ = "douban"
DESCRIPTION = "豆瓣" DESCRIPTION = "豆瓣"
META_URL = "https://book.douban.com/" META_URL = "https://book.douban.com/"
SEARCH_URL = "https://www.douban.com/j/search" SEARCH_JSON_URL = "https://www.douban.com/j/search"
SEARCH_URL = "https://www.douban.com/search"
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),") ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
AUTHORS_PATTERN = re.compile(r"作者|译者") AUTHORS_PATTERN = re.compile(r"作者|译者")
@ -52,6 +53,7 @@ class Douban(Metadata):
PUBLISHED_DATE_PATTERN = re.compile(r"出版年") PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
SERIES_PATTERN = re.compile(r"丛书") SERIES_PATTERN = re.compile(r"丛书")
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号") IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
TITTLE_XPATH = "//span[@property='v:itemreviewed']" TITTLE_XPATH = "//span[@property='v:itemreviewed']"
COVER_XPATH = "//a[@class='nbg']" COVER_XPATH = "//a[@class='nbg']"
@ -63,56 +65,90 @@ class Douban(Metadata):
session = requests.Session() session = requests.Session()
session.headers = { session.headers = {
'user-agent': 'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
} }
def search( def search(self,
self, query: str, generic_cover: str = "", locale: str = "en" query: str,
) -> Optional[List[MetaRecord]]: generic_cover: str = "",
locale: str = "en") -> List[MetaRecord]:
val = []
if self.active: if self.active:
log.debug(f"starting search {query} on douban") log.debug(f"start searching {query} on douban")
if title_tokens := list( if title_tokens := list(
self.get_title_tokens(query, strip_joiners=False) self.get_title_tokens(query, strip_joiners=False)):
):
query = "+".join(title_tokens) query = "+".join(title_tokens)
try: book_id_list = self._get_book_id_list_from_html(query)
r = self.session.get(
self.SEARCH_URL, params={"cat": 1001, "q": query}
)
r.raise_for_status()
except Exception as e: if not book_id_list:
log.warning(e) log.debug("No search results in Douban")
return None
results = r.json()
if results["total"] == 0:
return [] return []
book_id_list = [ with futures.ThreadPoolExecutor(
self.ID_PATTERN.search(item).group("id") max_workers=5, thread_name_prefix='douban') as executor:
for item in results["items"][:10] if self.ID_PATTERN.search(item)
]
with futures.ThreadPoolExecutor(max_workers=5) as executor:
fut = [ fut = [
executor.submit(self._parse_single_book, book_id, generic_cover) executor.submit(self._parse_single_book, book_id,
for book_id in book_id_list generic_cover) for book_id in book_id_list
] ]
val = [ val = [
future.result() future.result() for future in futures.as_completed(fut)
for future in futures.as_completed(fut) if future.result() if future.result()
] ]
return val return val
def _parse_single_book( def _get_book_id_list_from_html(self, query: str) -> List[str]:
self, id: str, generic_cover: str = "" try:
) -> Optional[MetaRecord]: r = self.session.get(self.SEARCH_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
html = etree.HTML(r.content.decode("utf8"))
result_list = html.xpath(self.COVER_XPATH)
return [
self.ID_PATTERN.search(item.get("onclick")).group("id")
for item in result_list[:10]
if self.ID_PATTERN.search(item.get("onclick"))
]
def _get_book_id_list_from_json(self, query: str) -> List[str]:
try:
r = self.session.get(self.SEARCH_JSON_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
results = r.json()
if results["total"] == 0:
return []
return [
self.ID_PATTERN.search(item).group("id")
for item in results["items"][:10] if self.ID_PATTERN.search(item)
]
def _parse_single_book(self,
id: str,
generic_cover: str = "") -> Optional[MetaRecord]:
url = f"https://book.douban.com/subject/{id}/" url = f"https://book.douban.com/subject/{id}/"
log.debug(f"start parsing {url}")
try: try:
r = self.session.get(url) r = self.session.get(url)
@ -136,7 +172,8 @@ class Douban(Metadata):
html = etree.HTML(r.content.decode("utf8")) html = etree.HTML(r.content.decode("utf8"))
match.title = html.xpath(self.TITTLE_XPATH)[0].text match.title = html.xpath(self.TITTLE_XPATH)[0].text
match.cover = html.xpath(self.COVER_XPATH)[0].attrib["href"] or generic_cover match.cover = html.xpath(
self.COVER_XPATH)[0].attrib["href"] or generic_cover
try: try:
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip()) rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
except Exception: except Exception:
@ -146,35 +183,39 @@ class Douban(Metadata):
tag_elements = html.xpath(self.TAGS_XPATH) tag_elements = html.xpath(self.TAGS_XPATH)
if len(tag_elements): if len(tag_elements):
match.tags = [tag_element.text for tag_element in tag_elements] match.tags = [tag_element.text for tag_element in tag_elements]
else:
match.tags = self._get_tags(html.text)
description_element = html.xpath(self.DESCRIPTION_XPATH) description_element = html.xpath(self.DESCRIPTION_XPATH)
if len(description_element): if len(description_element):
match.description = html2text(etree.tostring( match.description = html2text(
description_element[-1], encoding="utf8").decode("utf8")) etree.tostring(description_element[-1]).decode("utf8"))
info = html.xpath(self.INFO_XPATH) info = html.xpath(self.INFO_XPATH)
for element in info: for element in info:
text = element.text text = element.text
if self.AUTHORS_PATTERN.search(text): if self.AUTHORS_PATTERN.search(text):
next = element.getnext() next_element = element.getnext()
while next is not None and next.tag != "br": while next_element is not None and next_element.tag != "br":
match.authors.append(next.text) match.authors.append(next_element.text)
next = next.getnext() next_element = next_element.getnext()
elif self.PUBLISHER_PATTERN.search(text): elif self.PUBLISHER_PATTERN.search(text):
match.publisher = element.tail.strip() if publisher := element.tail.strip():
match.publisher = publisher
else:
match.publisher = element.getnext().text
elif self.SUBTITLE_PATTERN.search(text): elif self.SUBTITLE_PATTERN.search(text):
match.title = f'{match.title}:' + element.tail.strip() match.title = f'{match.title}:{element.tail.strip()}'
elif self.PUBLISHED_DATE_PATTERN.search(text): elif self.PUBLISHED_DATE_PATTERN.search(text):
match.publishedDate = self._clean_date(element.tail.strip()) match.publishedDate = self._clean_date(element.tail.strip())
elif self.SUBTITLE_PATTERN.search(text): elif self.SERIES_PATTERN.search(text):
match.series = element.getnext().text match.series = element.getnext().text
elif i_type := self.IDENTIFIERS_PATTERN.search(text): elif i_type := self.IDENTIFIERS_PATTERN.search(text):
match.identifiers[i_type.group()] = element.tail.strip() match.identifiers[i_type.group()] = element.tail.strip()
return match return match
def _clean_date(self, date: str) -> str: def _clean_date(self, date: str) -> str:
""" """
Clean up the date string to be in the format YYYY-MM-DD Clean up the date string to be in the format YYYY-MM-DD
@ -194,13 +235,24 @@ class Douban(Metadata):
if date[i].isdigit(): if date[i].isdigit():
digit.append(date[i]) digit.append(date[i])
elif digit: elif digit:
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}") ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
digit = [] digit = []
if digit: if digit:
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}") ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
moon = ls[0] moon = ls[0]
if len(ls)>1: if len(ls) > 1:
day = ls[1] day = ls[1]
return f"{year}-{moon}-{day}" return f"{year}-{moon}-{day}"
def _get_tags(self, text: str) -> List[str]:
tags = []
if criteria := self.CRITERIA_PATTERN.search(text):
tags.extend(
item.replace('7:', '') for item in criteria.group().split('|')
if item.startswith('7:'))
return tags

@ -19,6 +19,7 @@
# Google Books api document: https://developers.google.com/books/docs/v1/using # Google Books api document: https://developers.google.com/books/docs/v1/using
from typing import Dict, List, Optional from typing import Dict, List, Optional
from urllib.parse import quote from urllib.parse import quote
from datetime import datetime
import requests import requests
@ -81,7 +82,11 @@ class Google(Metadata):
match.description = result["volumeInfo"].get("description", "") match.description = result["volumeInfo"].get("description", "")
match.languages = self._parse_languages(result=result, locale=locale) match.languages = self._parse_languages(result=result, locale=locale)
match.publisher = result["volumeInfo"].get("publisher", "") match.publisher = result["volumeInfo"].get("publisher", "")
match.publishedDate = result["volumeInfo"].get("publishedDate", "") try:
datetime.strptime(result["volumeInfo"].get("publishedDate", ""), "%Y-%m-%d")
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
except ValueError:
match.publishedDate = ""
match.rating = result["volumeInfo"].get("averageRating", 0) match.rating = result["volumeInfo"].get("averageRating", 0)
match.series, match.series_index = "", 1 match.series, match.series_index = "", 1
match.tags = result["volumeInfo"].get("categories", []) match.tags = result["volumeInfo"].get("categories", [])
@ -103,6 +108,13 @@ class Google(Metadata):
def _parse_cover(result: Dict, generic_cover: str) -> str: def _parse_cover(result: Dict, generic_cover: str) -> str:
if result["volumeInfo"].get("imageLinks"): if result["volumeInfo"].get("imageLinks"):
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"] cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
# strip curl in cover
cover_url = cover_url.replace("&edge=curl", "")
# request 800x900 cover image (higher resolution)
cover_url += "&fife=w800-h900"
return cover_url.replace("http://", "https://") return cover_url.replace("http://", "https://")
return generic_cover return generic_cover

@ -49,10 +49,12 @@ class scholar(Metadata):
tokens = [quote(t.encode("utf-8")) for t in title_tokens] tokens = [quote(t.encode("utf-8")) for t in title_tokens]
query = " ".join(tokens) query = " ".join(tokens)
try: try:
scholarly.set_timeout(20)
scholarly.set_retries(2)
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10) scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
except Exception as e: except Exception as e:
log.warning(e) log.warning(e)
return None return list()
for result in scholar_gen: for result in scholar_gen:
match = self._parse_search_result( match = self._parse_search_result(
result=result, generic_cover="", locale=locale result=result, generic_cover="", locale=locale

@ -74,7 +74,7 @@ def register_user_with_oauth(user=None):
if len(all_oauth.keys()) == 0: if len(all_oauth.keys()) == 0:
return return
if user is None: if user is None:
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success") flash(_("Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
else: else:
for oauth_key in all_oauth.keys(): for oauth_key in all_oauth.keys():
# Find this OAuth token in the database, or create it # Find this OAuth token in the database, or create it
@ -134,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
# already bind with user, just login # already bind with user, just login
if oauth_entry.user: if oauth_entry.user:
login_user(oauth_entry.user) login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name) log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name), flash(_("Success! You are now logged in as: %(nickname)s", nickname= oauth_entry.user.name),
category="success") category="success")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
else: else:
@ -145,21 +145,21 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
try: try:
ub.session.add(oauth_entry) ub.session.add(oauth_entry)
ub.session.commit() ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") flash(_("Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
log.info("Link to {} Succeeded".format(provider_name)) log.info("Link to {} Succeeded".format(provider_name))
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
except Exception as ex: except Exception as ex:
log.error_or_exception(ex) log.error_or_exception(ex)
ub.session.rollback() ub.session.rollback()
else: else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") flash(_("Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account') log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
# return redirect(url_for('web.login')) # return redirect(url_for('web.login'))
# if config.config_public_reg: # if config.config_public_reg:
# return redirect(url_for('web.register')) # return redirect(url_for('web.register'))
# else: # else:
# flash(_(u"Public registration is not enabled"), category="error") # flash(_("Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url)) # return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError): except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url)) return redirect(url_for(redirect_url))
@ -194,15 +194,15 @@ def unlink_oauth(provider):
ub.session.delete(oauth_entry) ub.session.delete(oauth_entry)
ub.session.commit() ub.session.commit()
logout_oauth_user() logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") flash(_("Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
log.info("Unlink to {} Succeeded".format(oauth_check[provider])) log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
except Exception as ex: except Exception as ex:
log.error_or_exception(ex) log.error_or_exception(ex)
ub.session.rollback() ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") flash(_("Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound: except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id) log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error") flash(_("Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
def generate_oauth_blueprints(): def generate_oauth_blueprints():
@ -258,13 +258,13 @@ if ub.oauth_support:
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint']) @oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token): def github_logged_in(blueprint, token):
if not token: if not token:
flash(_(u"Failed to log in with GitHub."), category="error") flash(_("Failed to log in with GitHub."), category="error")
log.error("Failed to log in with GitHub") log.error("Failed to log in with GitHub")
return False return False
resp = blueprint.session.get("/user") resp = blueprint.session.get("/user")
if not resp.ok: if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error") flash(_("Failed to fetch user info from GitHub."), category="error")
log.error("Failed to fetch user info from GitHub") log.error("Failed to fetch user info from GitHub")
return False return False
@ -276,13 +276,13 @@ if ub.oauth_support:
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint']) @oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token): def google_logged_in(blueprint, token):
if not token: if not token:
flash(_(u"Failed to log in with Google."), category="error") flash(_("Failed to log in with Google."), category="error")
log.error("Failed to log in with Google") log.error("Failed to log in with Google")
return False return False
resp = blueprint.session.get("/oauth2/v2/userinfo") resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok: if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error") flash(_("Failed to fetch user info from Google."), category="error")
log.error("Failed to fetch user info from Google") log.error("Failed to fetch user info from Google")
return False return False
@ -295,8 +295,8 @@ if ub.oauth_support:
@oauth_error.connect_via(oauthblueprints[0]['blueprint']) @oauth_error.connect_via(oauthblueprints[0]['blueprint'])
def github_error(blueprint, error, error_description=None, error_uri=None): def github_error(blueprint, error, error_description=None, error_uri=None):
msg = ( msg = (
u"OAuth error from {name}! " "OAuth error from {name}! "
u"error={error} description={description} uri={uri}" "error={error} description={description} uri={uri}"
).format( ).format(
name=blueprint.name, name=blueprint.name,
error=error, error=error,
@ -308,8 +308,8 @@ if ub.oauth_support:
@oauth_error.connect_via(oauthblueprints[1]['blueprint']) @oauth_error.connect_via(oauthblueprints[1]['blueprint'])
def google_error(blueprint, error, error_description=None, error_uri=None): def google_error(blueprint, error, error_description=None, error_uri=None):
msg = ( msg = (
u"OAuth error from {name}! " "OAuth error from {name}! "
u"error={error} description={description} uri={uri}" "error={error} description={description} uri={uri}"
).format( ).format(
name=blueprint.name, name=blueprint.name,
error=error, error=error,
@ -329,10 +329,10 @@ def github_login():
if account_info.ok: if account_info.ok:
account_info_json = account_info.json() account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error") flash(_("GitHub Oauth error, please retry later."), category="error")
log.error("GitHub Oauth error, please retry later") log.error("GitHub Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e: except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"GitHub Oauth error: {}").format(e), category="error") flash(_("GitHub Oauth error: {}").format(e), category="error")
log.error(e) log.error(e)
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
@ -353,10 +353,10 @@ def google_login():
if resp.ok: if resp.ok:
account_info_json = resp.json() account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error") flash(_("Google Oauth error, please retry later."), category="error")
log.error("Google Oauth error, please retry later") log.error("Google Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e: except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"Google Oauth error: {}").format(e), category="error") flash(_("Google Oauth error: {}").format(e), category="error")
log.error(e) log.error(e)
return redirect(url_for('web.login')) return redirect(url_for('web.login'))

@ -329,7 +329,7 @@ def feed_format(book_id):
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_languagesindex(): def feed_languagesindex():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
if current_user.filter_language() == u"all": if current_user.filter_language() == "all":
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
else: else:
languages = calibre_db.session.query(db.Languages).filter( languages = calibre_db.session.query(db.Languages).filter(

@ -58,8 +58,8 @@ def remote_login():
ub.session.add(auth_token) ub.session.add(auth_token)
ub.session_commit() ub.session_commit()
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true) verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
log.debug(u"Remot Login request with token: %s", auth_token.auth_token) log.debug("Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token, return render_title_template('remote_login.html', title=_("Login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin") verify_url=verify_url, page="remotelogin")
@ -71,8 +71,8 @@ def verify_token(token):
# Token not found # Token not found
if auth_token is None: if auth_token is None:
flash(_(u"Token not found"), category="error") flash(_("Token not found"), category="error")
log.error(u"Remote Login token not found") log.error("Remote Login token not found")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
# Token expired # Token expired
@ -80,8 +80,8 @@ def verify_token(token):
ub.session.delete(auth_token) ub.session.delete(auth_token)
ub.session_commit() ub.session_commit()
flash(_(u"Token has expired"), category="error") flash(_("Token has expired"), category="error")
log.error(u"Remote Login token expired") log.error("Remote Login token expired")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
# Update token with user information # Update token with user information
@ -89,8 +89,8 @@ def verify_token(token):
auth_token.verified = True auth_token.verified = True
ub.session_commit() ub.session_commit()
flash(_(u"Success! Please return to your device"), category="success") flash(_("Success! Please return to your device"), category="success")
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id) log.debug("Remote Login token for userid %s verified", auth_token.user_id)
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@ -105,7 +105,7 @@ def token_verified():
# Token not found # Token not found
if auth_token is None: if auth_token is None:
data['status'] = 'error' data['status'] = 'error'
data['message'] = _(u"Token not found") data['message'] = _("Token not found")
# Token expired # Token expired
elif datetime.now() > auth_token.expiration: elif datetime.now() > auth_token.expiration:
@ -113,7 +113,7 @@ def token_verified():
ub.session_commit() ub.session_commit()
data['status'] = 'error' data['status'] = 'error'
data['message'] = _(u"Token has expired") data['message'] = _("Token has expired")
elif not auth_token.verified: elif not auth_token.verified:
data['status'] = 'not_verified' data['status'] = 'not_verified'
@ -126,8 +126,8 @@ def token_verified():
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name)) ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
data['status'] = 'success' data['status'] = 'success'
log.debug(u"Remote Login for userid %s succeded", user.id) log.debug("Remote Login for userid %s succeeded", user.id)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success") flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success")
response = make_response(json.dumps(data, ensure_ascii=False)) response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"

@ -59,7 +59,7 @@ def get_sidebar_config(kwargs=None):
"show_text": _('Show Top Rated Books'), "config_show": True}) "show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"page": "read", "show_text": _('Show read and unread'), "config_show": content}) "page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
@ -69,31 +69,31 @@ def get_sidebar_config(kwargs=None):
"show_text": _('Show Random Books'), "config_show": True}) "show_text": _('Show Random Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
"show_text": _('Show category selection'), "config_show": True}) "show_text": _('Show Category Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie", sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
"show_text": _('Show series selection'), "config_show": True}) "show_text": _('Show Series Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author", sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
"show_text": _('Show author selection'), "config_show": True}) "show_text": _('Show Author Section'), "config_show": True})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher", {"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
"show_text": _('Show publisher selection'), "config_show":True}) "show_text": _('Show Publisher Section'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang", sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'), "visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
"page": "language", "page": "language",
"show_text": _('Show language selection'), "config_show": True}) "show_text": _('Show Language Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate", sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
"visibility": constants.SIDEBAR_RATING, 'public': True, "visibility": constants.SIDEBAR_RATING, 'public': True,
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True}) "page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format", sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
"visibility": constants.SIDEBAR_FORMAT, 'public': True, "visibility": constants.SIDEBAR_FORMAT, 'public': True,
"page": "format", "show_text": _('Show file formats selection'), "config_show": True}) "page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content}) "show_text": _('Show Archived Books'), "config_show": content})
if not simple: if not simple:
sidebar.append( sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list", {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",

@ -19,11 +19,11 @@
import datetime import datetime
from . import config, constants from . import config, constants
from .services.background_scheduler import BackgroundScheduler, use_APScheduler from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
from .tasks.database import TaskReconnectDatabase from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks.metadata_backup import TaskBackupMetadata
def get_scheduled_tasks(reconnect=True): def get_scheduled_tasks(reconnect=True):
tasks = list() tasks = list()
@ -32,6 +32,10 @@ def get_scheduled_tasks(reconnect=True):
if reconnect: if reconnect:
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False]) tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
# ToDo make configurable. Generate metadata.opf file for each changed book
if False:
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
# Generate all missing book cover thumbnails # Generate all missing book cover thumbnails
if config.schedule_generate_book_covers: if config.schedule_generate_book_covers:
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True]) tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
@ -62,10 +66,10 @@ def register_scheduled_tasks(reconnect=True):
duration = config.schedule_duration duration = config.schedule_duration
# Register scheduled tasks # Register scheduled tasks
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger='cron', hour=start) scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start))
end_time = calclulate_end_time(start, duration) end_time = calclulate_end_time(start, duration)
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end_time.hour, scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute),
minute=end_time.minute) name="end scheduled task")
# Kick-off tasks, if they should currently be running # Kick-off tasks, if they should currently be running
if should_task_be_running(start, duration): if should_task_be_running(start, duration):
@ -91,6 +95,7 @@ def should_task_be_running(start, duration):
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60) end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
return start_time < now < end_time return start_time < now < end_time
def calclulate_end_time(start, duration): def calclulate_end_time(start, duration):
start_time = datetime.datetime.now().replace(hour=start, minute=0) start_time = datetime.datetime.now().replace(hour=start, minute=0)
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60) return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)

@ -45,7 +45,7 @@ def simple_search():
return render_title_template('search.html', return render_title_template('search.html',
searchterm="", searchterm="",
result_count=0, result_count=0,
title=_(u"Search"), title=_("Search"),
page="search") page="search")
@ -134,9 +134,10 @@ def adv_search_read_status(read_status):
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
except (KeyError, AttributeError, IndexError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column)) log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
flash(_("Custom Column No.{} does not exist in calibre database".format(config.config_read_column)), flash(_("Custom Column No.%(column)d does not exist in calibre database",
category="error") column=config.config_read_column),
return false() category="error")
return true()
return db_filter return db_filter
@ -184,18 +185,18 @@ def extend_search_term(searchterm,
searchterm.extend((author_name.replace('|', ','), book_title, publisher)) searchterm.extend((author_name.replace('|', ','), book_title, publisher))
if pub_start: if pub_start:
try: try:
searchterm.extend([_(u"Published after ") + searchterm.extend([_("Published after ") +
format_date(datetime.strptime(pub_start, "%Y-%m-%d"), format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
format='medium')]) format='medium')])
except ValueError: except ValueError:
pub_start = u"" pub_start = ""
if pub_end: if pub_end:
try: try:
searchterm.extend([_(u"Published before ") + searchterm.extend([_("Published before ") +
format_date(datetime.strptime(pub_end, "%Y-%m-%d"), format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
format='medium')]) format='medium')])
except ValueError: except ValueError:
pub_end = u"" pub_end = ""
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf} elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
for key, db_element in elements.items(): for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all() tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
@ -213,11 +214,11 @@ def extend_search_term(searchterm,
language_names = calibre_db.speaking_language(language_names) language_names = calibre_db.speaking_language(language_names)
searchterm.extend(language.name for language in language_names) searchterm.extend(language.name for language in language_names)
if rating_high: if rating_high:
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)]) searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
if rating_low: if rating_low:
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)]) searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
if read_status: if read_status:
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)]) searchterm.extend([_("Read Status = %(status)s", status=read_status)])
searchterm.extend(ext for ext in tags['include_extension']) searchterm.extend(ext for ext in tags['include_extension'])
searchterm.extend(ext for ext in tags['exclude_extension']) searchterm.extend(ext for ext in tags['exclude_extension'])
# handle custom columns # handle custom columns
@ -266,19 +267,19 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
column_start = term.get('custom_column_' + str(c.id) + '_start') column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end') column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start: if column_start:
search_term.extend([u"{} >= {}".format(c.name, search_term.extend(["{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(), format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium') format='medium')
)]) )])
cc_present = True cc_present = True
if column_end: if column_end:
search_term.extend([u"{} <= {}".format(c.name, search_term.extend(["{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(), format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium') format='medium')
)]) )])
cc_present = True cc_present = True
elif term.get('custom_column_' + str(c.id)): elif term.get('custom_column_' + str(c.id)):
search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))]) search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True cc_present = True
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \ if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
@ -338,7 +339,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
pagination=pagination, pagination=pagination,
entries=entries, entries=entries,
result_count=result_count, result_count=result_count,
title=_(u"Advanced Search"), page="advsearch", title=_("Advanced Search"), page="advsearch",
order=order[1]) order=order[1])
@ -365,12 +366,12 @@ def render_prepare_search_form(cc):
.filter(calibre_db.common_filters()) \ .filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\ .group_by(db.Data.format)\
.order_by(db.Data.format).all() .order_by(db.Data.format).all()
if current_user.filter_language() == u"all": if current_user.filter_language() == "all":
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
else: else:
languages = None languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions, return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch") series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None): def render_search_results(term, offset=None, order=None, limit=None):
@ -388,7 +389,7 @@ def render_search_results(term, offset=None, order=None, limit=None):
adv_searchterm=term, adv_searchterm=term,
entries=entries, entries=entries,
result_count=result_count, result_count=result_count,
title=_(u"Search"), title=_("Search"),
page="search", page="search",
order=order[1]) order=order[1])

@ -23,6 +23,8 @@ from .worker import WorkerThread
try: try:
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
use_APScheduler = True use_APScheduler = True
except (ImportError, RuntimeError) as e: except (ImportError, RuntimeError) as e:
use_APScheduler = False use_APScheduler = False
@ -47,31 +49,31 @@ class BackgroundScheduler:
return cls._instance return cls._instance
def schedule(self, func, trigger, name=None, **trigger_args): def schedule(self, func, trigger, name=None):
if use_APScheduler: if use_APScheduler:
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args) return self.scheduler.add_job(func=func, trigger=trigger, name=name)
# Expects a lambda expression for the task # Expects a lambda expression for the task
def schedule_task(self, task, user=None, name=None, hidden=False, trigger='cron', **trigger_args): def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
if use_APScheduler: if use_APScheduler:
def scheduled_task(): def scheduled_task():
worker_task = task() worker_task = task()
worker_task.scheduled = True worker_task.scheduled = True
WorkerThread.add(user, worker_task, hidden=hidden) WorkerThread.add(user, worker_task, hidden=hidden)
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args) return self.schedule(func=scheduled_task, trigger=trigger, name=name)
# Expects a list of lambda expressions for the tasks # Expects a list of lambda expressions for the tasks
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args): def schedule_tasks(self, tasks, user=None, trigger=None):
if use_APScheduler: if use_APScheduler:
for task in tasks: for task in tasks:
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2], **trigger_args) self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
# Expects a lambda expression for the task # Expects a lambda expression for the task
def schedule_task_immediately(self, task, user=None, name=None, hidden=False): def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
if use_APScheduler: if use_APScheduler:
def immediate_task(): def immediate_task():
WorkerThread.add(user, task(), hidden) WorkerThread.add(user, task(), hidden)
return self.schedule(func=immediate_task, trigger='date', name=name) return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
# Expects a list of lambda expressions for the tasks # Expects a list of lambda expressions for the tasks
def schedule_tasks_immediately(self, tasks, user=None): def schedule_tasks_immediately(self, tasks, user=None):

@ -46,13 +46,13 @@ def add_to_shelf(shelf_id, book_id):
if shelf is None: if shelf is None:
log.error("Invalid shelf specified: %s", shelf_id) log.error("Invalid shelf specified: %s", shelf_id)
if not xhr: if not xhr:
flash(_(u"Invalid shelf specified"), category="error") flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return "Invalid shelf specified", 400 return "Invalid shelf specified", 400
if not check_shelf_edit_permissions(shelf): if not check_shelf_edit_permissions(shelf):
if not xhr: if not xhr:
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error") flash(_("Sorry you are not allowed to add a book to that shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return "Sorry you are not allowed to add a book to the that shelf", 403 return "Sorry you are not allowed to add a book to the that shelf", 403
@ -61,7 +61,7 @@ def add_to_shelf(shelf_id, book_id):
if book_in_shelf: if book_in_shelf:
log.error("Book %s is already part of %s", book_id, shelf) log.error("Book %s is already part of %s", book_id, shelf)
if not xhr: if not xhr:
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") flash(_("Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return "Book is already part of the shelf: %s" % shelf.name, 400 return "Book is already part of the shelf: %s" % shelf.name, 400
@ -79,14 +79,14 @@ def add_to_shelf(shelf_id, book_id):
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ: if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
else: else:
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if not xhr: if not xhr:
log.debug("Book has been added to shelf: {}".format(shelf.name)) log.debug("Book has been added to shelf: {}".format(shelf.name))
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") flash(_("Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ: if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
else: else:
@ -100,12 +100,12 @@ def search_to_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None: if shelf is None:
log.error("Invalid shelf specified: {}".format(shelf_id)) log.error("Invalid shelf specified: {}".format(shelf_id))
flash(_(u"Invalid shelf specified"), category="error") flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf): if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the shelf".format(shelf.name)) log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
flash(_(u"You are not allowed to add a book to the shelf"), category="error") flash(_("You are not allowed to add a book to the shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]: if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
@ -123,7 +123,7 @@ def search_to_shelf(shelf_id):
if not books_for_shelf: if not books_for_shelf:
log.error("Books are already part of {}".format(shelf.name)) log.error("Books are already part of {}".format(shelf.name))
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") flash(_("Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0 maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
@ -135,14 +135,14 @@ def search_to_shelf(shelf_id):
try: try:
ub.session.merge(shelf) ub.session.merge(shelf)
ub.session.commit() ub.session.commit()
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success") flash(_("Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
else: else:
log.error("Could not add books to shelf: {}".format(shelf.name)) log.error("Could not add books to shelf: {}".format(shelf.name))
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error") flash(_("Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@ -182,13 +182,13 @@ def remove_from_shelf(shelf_id, book_id):
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ: if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
else: else:
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if not xhr: if not xhr:
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") flash(_("Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ: if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"]) return redirect(request.environ["HTTP_REFERER"])
else: else:
@ -197,7 +197,7 @@ def remove_from_shelf(shelf_id, book_id):
else: else:
if not xhr: if not xhr:
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name)) log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
flash(_(u"Sorry you are not allowed to remove a book from this shelf"), flash(_("Sorry you are not allowed to remove a book from this shelf"),
category="error") category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return "Sorry you are not allowed to remove a book from this shelf", 403 return "Sorry you are not allowed to remove a book from this shelf", 403
@ -207,7 +207,7 @@ def remove_from_shelf(shelf_id, book_id):
@login_required @login_required
def create_shelf(): def create_shelf():
shelf = ub.Shelf() shelf = ub.Shelf()
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate") return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"]) @shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@ -215,9 +215,9 @@ def create_shelf():
def edit_shelf(shelf_id): def edit_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if not check_shelf_edit_permissions(shelf): if not check_shelf_edit_permissions(shelf):
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error") flash(_("Sorry you are not allowed to edit this shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id) return create_edit_shelf(shelf, page_title=_("Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"]) @shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
@ -232,7 +232,7 @@ def delete_shelf(shelf_id):
except InvalidRequestError as e: except InvalidRequestError as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@ -263,13 +263,13 @@ def order_shelf(shelf_id):
for book in books_in_shelf: for book in books_in_shelf:
setattr(book, 'order', to_save[str(book.book_id)]) setattr(book, 'order', to_save[str(book.book_id)])
counter += 1 counter += 1
# if order diffrent from before -> shelf.last_modified = datetime.utcnow() # if order different from before -> shelf.last_modified = datetime.utcnow()
try: try:
ub.session.commit() ub.session.commit()
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
result = list() result = list()
if shelf: if shelf:
@ -278,7 +278,7 @@ def order_shelf(shelf_id):
.add_columns(calibre_db.common_filters().label("visible")) \ .add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all() .filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result, return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), title=_("Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder") shelf=shelf, page="shelforder")
else: else:
abort(404) abort(404)
@ -310,7 +310,7 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on": if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error") flash(_("Sorry you are not allowed to create a public shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
is_public = 1 if to_save.get("is_public") == "on" else 0 is_public = 1 if to_save.get("is_public") == "on" else 0
if config.config_kobo_sync: if config.config_kobo_sync:
@ -327,24 +327,24 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
shelf.user_id = int(current_user.id) shelf.user_id = int(current_user.id)
ub.session.add(shelf) ub.session.add(shelf)
shelf_action = "created" shelf_action = "created"
flash_text = _(u"Shelf %(title)s created", title=shelf_title) flash_text = _("Shelf %(title)s created", title=shelf_title)
else: else:
shelf_action = "changed" shelf_action = "changed"
flash_text = _(u"Shelf %(title)s changed", title=shelf_title) flash_text = _("Shelf %(title)s changed", title=shelf_title)
try: try:
ub.session.commit() ub.session.commit()
log.info(u"Shelf {} {}".format(shelf_title, shelf_action)) log.info("Shelf {} {}".format(shelf_title, shelf_action))
flash(flash_text, category="success") flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as ex: except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback() ub.session.rollback()
log.error_or_exception(ex) log.error_or_exception(ex)
log.error_or_exception("Settings Database error: {}".format(ex)) log.error_or_exception("Settings Database error: {}".format(ex))
flash(_(u"Database error: %(error)s.", error=ex.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
except Exception as ex: except Exception as ex:
ub.session.rollback() ub.session.rollback()
log.error_or_exception(ex) log.error_or_exception(ex)
flash(_(u"There was an error"), category="error") flash(_("There was an error"), category="error")
return render_title_template('shelf_edit.html', return render_title_template('shelf_edit.html',
shelf=shelf, shelf=shelf,
title=page_title, title=page_title,
@ -366,7 +366,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
if not is_shelf_name_unique: if not is_shelf_name_unique:
log.error("A public shelf with the name '{}' already exists.".format(title)) log.error("A public shelf with the name '{}' already exists.".format(title))
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title), flash(_("A public shelf with the name '%(title)s' already exists.", title=title),
category="error") category="error")
else: else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \ is_shelf_name_unique = ub.session.query(ub.Shelf) \
@ -377,7 +377,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
if not is_shelf_name_unique: if not is_shelf_name_unique:
log.error("A private shelf with the name '{}' already exists.".format(title)) log.error("A private shelf with the name '{}' already exists.".format(title))
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title), flash(_("A private shelf with the name '%(title)s' already exists.", title=title),
category="error") category="error")
return is_shelf_name_unique return is_shelf_name_unique
@ -454,14 +454,14 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e)) log.error_or_exception("Settings Database error: {}".format(e))
flash(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return render_title_template(page, return render_title_template(page,
entries=result, entries=result,
pagination=pagination, pagination=pagination,
title=_(u"Shelf: '%(name)s'", name=shelf.name), title=_("Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, shelf=shelf,
page="shelf") page="shelf")
else: else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") flash(_("Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))

@ -3290,9 +3290,11 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
-ms-transform-origin: center top; -ms-transform-origin: center top;
transform-origin: center top; transform-origin: center top;
border: 0; border: 0;
left: 0 !important;
overflow-y: auto; overflow-y: auto;
} }
.dropdown-menu:not(.datepicker-dropdown):not(.profileDropli) {
left: 0 !important;
}
#add-to-shelves { #add-to-shelves {
max-height: calc(100% - 120px); max-height: calc(100% - 120px);
overflow-y: auto; overflow-y: auto;
@ -4423,38 +4425,6 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
left: 49px; left: 49px;
margin-top: 5px margin-top: 5px
} }
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after, body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
color: hsla(0, 0%, 100%, .7);
cursor: pointer;
display: block;
font-family: plex-icons-new, serif;
font-size: 20px;
font-stretch: 100%;
font-style: normal;
font-variant-caps: normal;
font-variant-east-asian: normal;
font-variant-numeric: normal;
font-weight: 400;
height: 60px;
letter-spacing: normal;
line-height: 60px;
position: absolute
}
body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
content: "\EA30";
-webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal;
left: 20px
}
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after {
content: "\EA2F";
-webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal;
left: 60px
}
} }
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before { body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before {

@ -22,3 +22,7 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
padding: 0 0; padding: 0 0;
line-height: 15px; line-height: 15px;
} }
input.datepicker {color: transparent}
input.datepicker:focus {color: transparent}
input.datepicker:focus + input {color: #555}

@ -1,6 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8
9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>

Before

Width:  |  Height:  |  Size: 461 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

@ -1,16 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)">
<path
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
</path>
<path
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
</path>
<circle
cx="8" cy="5" r="1.188">
</circle>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>

Before

Width:  |  Height:  |  Size: 255 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>

Before

Width:  |  Height:  |  Size: 256 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>

Before

Width:  |  Height:  |  Size: 231 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>

Before

Width:  |  Height:  |  Size: 521 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>

Before

Width:  |  Height:  |  Size: 302 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>

Before

Width:  |  Height:  |  Size: 307 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>

Before

Width:  |  Height:  |  Size: 509 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>

Before

Width:  |  Height:  |  Size: 505 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M6 3c-1 0-1.5.5-1.5 1.5v7c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5v-7c0-1-.5-1.5-1.5-1.5z"/></svg>

Before

Width:  |  Height:  |  Size: 196 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M10.56 3.5C9.56 3.5 9 4 9 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.93 1.2c.8 0 1.4.2 1.8.64.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.44-.2.3-.6.6-1 .93l-.6.4c-.4.3-.6.4-.7.55-.1.1-.2.2-.3.4h3.2v1.27h-5c0-.5.1-1 .3-1.43.2-.49.7-1 1.5-1.54.7-.5 1.1-.8 1.3-1.02.3-.3.4-.7.4-1.05 0-.3-.1-.6-.3-.77-.2-.2-.4-.3-.7-.3-.4 0-.7.2-.9.5-.1.2-.1.5-.2.9h-1.4c0-.6.2-1.1.3-1.5.4-.7 1.1-1.1 2-1.1zM1.54 3.5C.54 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.54 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm1.8 1.125H4.5V12H3V6.9H1.3v-1c.5 0 .8 0 .97-.03.33-.07.53-.17.73-.37.1-.2.2-.3.25-.5.05-.2.05-.3.05-.3z"/></svg>

Before

Width:  |  Height:  |  Size: 705 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M4 16V2s0-1 1-1h6s1 0 1 1v14l-4-5z"/></svg>

Before

Width:  |  Height:  |  Size: 142 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="m14 9h-6c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm-5.2-8h-3.8c-1.3 0-1.3 2 0 2h1.7zm-6.8 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.3 1.7-0.7 0-0.5-0.4-1-1-1zm3 8c-1 0-1.3 1-0.7 1.7 0.6 0.6 1.7 0.2 1.7-0.7 0-0.5-0.4-1-1-1zm0.3-4h-0.3c-1.4 0-1.4 2 0 2h2.3zm-3.3 0c-0.9 0-1.4 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.7 0-0.6-0.5-1-1-1zm12 8h-9c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zm-12 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.712 0-0.5-0.4-1-1-1z"/><path d="m7.37 4.838 3.93-3.911v2.138h3.629v3.546h-3.629v2.138l-3.93-3.911"/></svg>

After

Width:  |  Height:  |  Size: 581 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 3h-2v2h2v8H2V5h7V3h-.849L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM2 3h3.219l1.072 1H2z"></path><path d="M8.146 6.146a.5.5 0 0 0 0 .707l2 2a.5.5 0 0 0 .707 0l2-2a.5.5 0 1 0-.707-.707L11 7.293V.5a.5.5 0 0 0-1 0v6.793L8.854 6.146a.5.5 0 0 0-.708 0z"></path></svg>

Before

Width:  |  Height:  |  Size: 651 B

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<g>
<g transform="scale(0.03125)">
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
</g>
</g>
<g>
<g transform="scale(0.03125)">
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 16 16">
<g>
<g transform="scale(0.03125)">
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 804 B

@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="rgba(255,255,255,1)"><path d="M8 11a1 1 0 01-.707-.293l-2.99-2.99c-.91-.942.471-2.324 1.414-1.414L8 8.586l2.283-2.283c.943-.91 2.324.472 1.414 1.414l-2.99 2.99A1 1 0 018 11z"/></svg>

Before

Width:  |  Height:  |  Size: 251 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14.859 3.2a1.335 1.335 0 0 1-1.217.8H13v1h1v8H2V5h8V4h-.642a1.365 1.365 0 0 1-1.325-1.11L6.584 1.538A2 2 0 0 0 5.219 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-1.141-1.8zM2 3h3.219l1.072 1H2zm7.854-.146L11 1.707V8.5a.5.5 0 0 0 1 0V1.707l1.146 1.146a.5.5 0 1 0 .707-.707l-2-2a.5.5 0 0 0-.707 0l-2 2a.5.5 0 0 0 .707.707z"></path></svg>

Before

Width:  |  Height:  |  Size: 686 B

@ -1,8 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)"><path transform='rotate(90) translate(0, -16)'
d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293
4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z"></path></svg>

Before

Width:  |  Height:  |  Size: 517 B

@ -1,13 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
16"
fill="rgba(255,255,255,1)">
<path
transform='rotate(90) translate(0, -16)'
d="M15 7H3.414l4.293-4.293a1 1 0 0
0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0
0 0-2z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 517 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M.5 1H7s0-1 1-1 1 1 1 1h6.5s.5 0 .5.5-.5.5-.5.5H.5S0 2 0 1.5.5 1 .5 1zM1 3h14v7c0 2-1 2-2 2H3c-1 0-2 0-2-2zm5 1v7l6-3.5zM3.72 15.33l.53-2s0-.5.65-.35c.51.13.38.63.38.63l-.53 2s0 .5-.64.35c-.53-.13-.39-.63-.39-.63zM11.24 15.61l-.53-1.99s0-.5.38-.63c.51-.13.64.35.64.35l.53 2s0 .5-.38.63c-.5.13-.64-.35-.65-.35z"/></svg>

Before

Width:  |  Height:  |  Size: 417 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 5h-1V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v4H2a2 2 0 0 0-2 2v5h3v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3h3V7a2 2 0 0 0-2-2zM2.5 8a.5.5 0 1 1 .5-.5.5.5 0 0 1-.5.5zm9.5 7H4v-5h8zm0-10H4V1h8zm-6.5 7h4a.5.5 0 0 0 0-1h-4a.5.5 0 1 0 0 1zm0 2h5a.5.5 0 0 0 0-1h-5a.5.5 0 1 0 0 1z"></path></svg>

Before

Width:  |  Height:  |  Size: 610 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M15.707 14.293l-4.822-4.822a6.019 6.019 0 1 0-1.414 1.414l4.822 4.822a1 1 0 0 0 1.414-1.414zM6 10a4 4 0 1 1 4-4 4 4 0 0 1-4 4z"></path></svg>

Before

Width:  |  Height:  |  Size: 472 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M8.707 7.293l-5-5a1 1 0 0 0-1.414 1.414L6.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414zm6 0l-5-5a1 1 0 0 0-1.414 1.414L12.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414z"></path></svg>

Before

Width:  |  Height:  |  Size: 549 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M3 1h10a3.008 3.008 0 0 1 3 3v8a3.009 3.009 0 0 1-3 3H3a3.005 3.005 0 0 1-3-3V4a3.013 3.013 0 0 1 3-3zm11 11V4a1 1 0 0 0-1-1H8v10h5a1 1 0 0 0 1-1zM2 12a1 1 0 0 0 1 1h4V3H3a1 1 0 0 0-1 1v8z"></path><path d="M3.5 5h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm0 2h2a.5.5 0 0 0 0-1h-2a.5.5 0 0 0 0 1zm1 2h1a.5.5 0 0 0 0-1h-1a.5.5 0 0 0 0 1z"></path></svg>

Before

Width:  |  Height:  |  Size: 674 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M6.2 2s.5-.5 1.06 0c.5.5 0 1 0 1l-4.6 4.61s-2.5 2.5 0 5 5 0 5 0L13.8 6.4s1.6-1.6 0-3.2-3.2 0-3.2 0L5.8 8s-.7.7 0 1.4 1.4 0 1.4 0l3.9-3.9s.6-.5 1 0c.5.5 0 1 0 1l-3.8 4s-1.8 1.8-3.5 0C3 8.7 4.8 7 4.8 7l4.7-4.9s2.7-2.6 5.3 0c2.6 2.6 0 5.3 0 5.3l-6.2 6.3s-3.5 3.5-7 0 0-7 0-7z"/></svg>

Before

Width:  |  Height:  |  Size: 380 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.233 4.233" height="16" width="16" fill="rgba(255,255,255,1)"><path d="M.15 2.992c-.198.1-.2.266-.002.365l1.604.802a.93.93 0 00.729-.001l1.602-.801c.198-.1.197-.264 0-.364l-.695-.348c-1.306.595-2.542 0-2.542 0m-.264.53l.658-.329c.6.252 1.238.244 1.754 0l.659.329-1.536.768zM.15 1.935c-.198.1-.198.265 0 .364l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363l-.694-.35c-1.14.56-2.546.001-2.546.001m-.264.53l.664-.332c.52.266 1.261.235 1.75.002l.659.33-1.537.768zM.15.877c-.198.099-.198.264 0 .363l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363L2.481.075a.926.926 0 00-.727 0zm.43.182L2.117.29l1.538.769-1.538.768z"/></svg>

Before

Width:  |  Height:  |  Size: 712 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
fill="rgba(255,255,255,1)"><path d="M14 9H8c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm0-8H5C3.7 1 3.7 3 5 3h9c1.3 0 1.3-2 0-2zM2 1C1 1 .7 2 1.3 2.7 2 3.3 3 3 3 2c0-.5-.4-1-1-1zm3 8c-1 0-1.3 1-.7 1.7.6.6 1.7.2 1.7-.7 0-.5-.4-1-1-1zM14 5H5C3.6 5 3.6 7 5 7h9c1.3 0 1.3-2 0-2zM2 5c-.9 0-1.4 1-.7 1.7C2 7.3 3 6.9 3 6c0-.6-.5-1-1-1zM14 13H5c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zM2 13c-1 0-1.3 1-.7 1.7.7.6 1.7.2 1.7-.712 0-.5-.4-1-1-1z"/></svg>

Before

Width:  |  Height:  |  Size: 493 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><g style="--darkreader-inline-fill:rgba(81, 82, 83, 0.8);" data-darkreader-inline-fill=""><rect x="1" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="1" y="9" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="9" width="6" height="6" rx="1" ry="1"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 662 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><path d="M14 7H9V2a1 1 0 0 0-2 0v5H2a1 1 0 0 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2z"></path></svg>

Before

Width:  |  Height:  |  Size: 424 B

@ -1,5 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="rgba(255,255,255,1)"><rect x="2" y="7" width="12" height="2" rx="1"></rect></svg>

Before

Width:  |  Height:  |  Size: 382 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M13 9L6 5v8z"/></svg>

Before

Width:  |  Height:  |  Size: 120 B

@ -1,2 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
fill="rgba(255,255,255,1)"><path d="M10 13l4-7H6z"/></svg>

Before

Width:  |  Height:  |  Size: 121 B

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
.fontSizeWrapper {
position: relative;
}
.slider {
position: absolute;
top: 50%;
transform: translate(0,-50%);
width: 90%;
height: 60px;
background: transparent;
border-radius: 20px;
display: flex;
align-items: center;
box-shadow: 0px 15px 40px #7E6D5766;
}
.slider label {
font-size: 20px;
font-weight: 400;
font-family: Open Sans;
padding-right: 10px;
color: white;
}
.slider input[type="range"] {
width: 80%;
height: 5px;
background: black;
border: none;
outline: none;
}

@ -16,7 +16,6 @@
*/ */
// Move advanced search to side-menu // Move advanced search to side-menu
$("a[href*='advanced']").parent().insertAfter("#nav_new"); $("a[href*='advanced']").parent().insertAfter("#nav_new");
$("body").addClass("blur");
$("body.stat").addClass("stats"); $("body.stat").addClass("stats");
$("body.config").addClass("admin"); $("body.config").addClass("admin");
$("body.uiconfig").addClass("admin"); $("body.uiconfig").addClass("admin");
@ -29,8 +28,8 @@ $("body > div.container-fluid > div > div.col-sm-10 > div.filterheader").attr("s
// Back button // Back button
curHref = window.location.href.split("/"); curHref = window.location.href.split("/");
prevHref = document.referrer.split("/"); prevHref = document.referrer.split("/");
$(".navbar-form.navbar-left") $(".plexBack a").attr('href', encodeURI(document.referrer));
.before('<div class="plexBack"><a href="' + encodeURI(document.referrer) + '"></a></div>');
if (history.length === 1 || if (history.length === 1 ||
curHref[0] + curHref[0] +
curHref[1] + curHref[1] +
@ -44,14 +43,9 @@ if (history.length === 1 ||
//Weird missing a after pressing back from edit. //Weird missing a after pressing back from edit.
setTimeout(function () { setTimeout(function () {
if ($(".plexBack a").length < 1) { $(".plexBack a").attr('href', encodeURI(document.referrer));
$(".plexBack").append('<a href="' + encodeURI(document.referrer) + '"></a>');
}
}, 10); }, 10);
// Home button
$(".plexBack").before('<div class="home-btn"></div>');
$("a.navbar-brand").clone().appendTo(".home-btn").empty().removeClass("navbar-brand");
///////////////////////////////// /////////////////////////////////
// Start of Book Details Work // // Start of Book Details Work //
/////////////////////////////// ///////////////////////////////
@ -326,13 +320,8 @@ url = window.location.pathname
// Move create shelf // Move create shelf
$("#nav_createshelf").prependTo(".your-shelves"); $("#nav_createshelf").prependTo(".your-shelves");
// Create drop-down for profile and move elements to it // Move About link it the profile dropdown
$("#main-nav") $(".profileDropli #top_user").parent().after($("#nav_about").addClass("dropdown"))
.prepend('<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>');
$("#top_user").parent().addClass("dropdown").appendTo(".profileDropli");
$("#nav_about").addClass("dropdown").appendTo(".profileDropli");
$("#register").parent().addClass("dropdown").appendTo(".profileDropli");
$("#logout").parent().addClass("dropdown").appendTo(".profileDropli");
// Remove the modals except from some areas where they are needed // Remove the modals except from some areas where they are needed
bodyClass = $("body").attr("class").split(" "); bodyClass = $("body").attr("class").split(" ");
@ -666,7 +655,7 @@ $("#sendbtn").attr({
$("#sendbtn2").attr({ $("#sendbtn2").attr({
"data-toggle-two": "tooltip", "data-toggle-two": "tooltip",
"title": $("#sendbtn2").text(), // "Send to E-Reader", "title": $("#sendbtn2").text(), // "Send to eReader",
"data-placement": "bottom", "data-placement": "bottom",
"data-viewport": ".btn-toolbar" "data-viewport": ".btn-toolbar"
}) })

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -125,7 +125,7 @@ function loadArchiveFormats(formats, cb) {
_loaded_archive_formats.push(archive_format); _loaded_archive_formats.push(archive_format);
break; break;
case 'zip': case 'zip':
loadScript(path + 'jszip.js', checkForLoadDone); loadScript(path + 'jszip.min.js', checkForLoadDone);
_loaded_archive_formats.push(archive_format); _loaded_archive_formats.push(archive_format);
break; break;
case 'tar': case 'tar':

@ -163,6 +163,14 @@ kthoom.ImageFile = function(file) {
this.mimeType = undefined; this.mimeType = undefined;
break; break;
} }
// Reset mime type for special files originating from Apple devices
// This folder may contain files having image extensions (for example .jpg) but those files are not actual images
// Trying to view these files cause corrupted/empty pages in the comic reader and files should be ignored
if (this.filename.indexOf("__MACOSX") !== -1) {
this.mimeType = undefined;
}
if ( this.mimeType !== undefined) { if ( this.mimeType !== undefined) {
this.dataURI = createURLFromArray(file.fileData, this.mimeType); this.dataURI = createURLFromArray(file.fileData, this.mimeType);
} }

File diff suppressed because one or more lines are too long

@ -177,6 +177,9 @@
whileplaying: function () { whileplaying: function () {
// get csrf_token
let csrf_token = $("input[name='csrf_token']").val();
//This sends a bookmark update to calibreweb every 30 seconds. //This sends a bookmark update to calibreweb every 30 seconds.
if (this.progressBuffer == undefined) { if (this.progressBuffer == undefined) {
@ -187,7 +190,10 @@
$.ajax(calibre.bookmarkUrl, { $.ajax(calibre.bookmarkUrl, {
method: "post", method: "post",
data: { bookmark: this.position } data: {
csrf_token: csrf_token,
bookmark: this.position
}
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {
console.error(error); console.error(error);
}); });
@ -313,14 +319,14 @@
}, },
onstop: function () { onstop: function () {
$.ajax(calibre.bookmarkUrl, { $.ajax(calibre.bookmarkUrl, {
method: "post", method: "post",
data: { bookmark: this.position } data: { bookmark: this.position }
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {
console.error(error); console.error(error);
}); });
utils.css.remove(dom.o, 'playing'); utils.css.remove(dom.o, 'playing');
}, },

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.gl={days:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],daysShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],daysMin:["Do","Lu","Ma","Me","Xo","Ve","Sa"],months:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthsShort:["Xan","Feb","Mar","Abr","Mai","Xun","Xul","Ago","Sep","Out","Nov","Dec"],today:"Hoxe",clear:"Limpar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery);

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery);

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.no={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthsShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],today:"i dag",monthsTitle:"Måneder",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.vi={days:["Chủ nhật","Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy"],daysShort:["CN","Thứ 2","Thứ 3","Thứ 4","Thứ 5","Thứ 6","Thứ 7"],daysMin:["CN","T2","T3","T4","T5","T6","T7"],months:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],monthsShort:["Th1","Th2","Th3","Th4","Th5","Th6","Th7","Th8","Th9","Th10","Th11","Th12"],today:"Hôm nay",clear:"Xóa",format:"dd/mm/yyyy"}}(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,412 @@
/*!
* TinyMCE Language Pack
*
* Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
* Licensed under the Tiny commercial license. See https://www.tiny.cloud/legal/
*/
tinymce.addI18n('nb_NO', {
"Redo": "Gjør om",
"Undo": "Angre",
"Cut": "Klipp ut",
"Copy": "Kopier",
"Paste": "Lim inn",
"Select all": "Marker alt",
"New document": "Nytt dokument",
"Ok": "",
"Cancel": "Avbryt",
"Visual aids": "Visuelle hjelpemidler",
"Bold": "Fet",
"Italic": "Kursiv",
"Underline": "Understreking",
"Strikethrough": "Gjennomstreking",
"Superscript": "Hevet skrift",
"Subscript": "Senket skrift",
"Clear formatting": "Fjern formateringer",
"Remove": "",
"Align left": "Venstrejuster",
"Align center": "Midtstill",
"Align right": "Høyrejuster",
"No alignment": "",
"Justify": "Blokkjuster",
"Bullet list": "Punktliste",
"Numbered list": "Nummerliste",
"Decrease indent": "Reduser innrykk",
"Increase indent": "Øk innrykk",
"Close": "Lukk",
"Formats": "Stiler",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "Nettleseren din støtter ikke direkte tilgang til utklippsboken. Bruk istedet tastatursnarveiene Ctrl+X/C/V.",
"Headings": "Overskrifter",
"Heading 1": "Overskrift 1",
"Heading 2": "Overskrift 2",
"Heading 3": "Overskrift 3",
"Heading 4": "Overskrift 4",
"Heading 5": "Overskrift 5",
"Heading 6": "Overskrift 6",
"Preformatted": "Forhåndsformatert",
"Div": "",
"Pre": "",
"Code": "Kode",
"Paragraph": "Avsnitt",
"Blockquote": "",
"Inline": "Innkapslet",
"Blocks": "Blokker",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Lim inn er nå i ren tekst-modus. Kopiert innhold vil bli limt inn som ren tekst inntil du slår av dette valget.",
"Fonts": "Fonter",
"Font sizes": "",
"Class": "Klasse",
"Browse for an image": "Søk etter bilde",
"OR": "",
"Drop an image here": "Slipp et bilde her",
"Upload": "Last opp",
"Uploading image": "",
"Block": "Blokk",
"Align": "Juster",
"Default": "Standard",
"Circle": "Sirkel",
"Disc": "Disk",
"Square": "Firkant",
"Lower Alpha": "Små bokstaver",
"Lower Greek": "Greske minuskler",
"Lower Roman": "Små romertall",
"Upper Alpha": "Store bokstaver",
"Upper Roman": "Store romertall",
"Anchor...": "Lenke",
"Anchor": "",
"Name": "Navn",
"ID": "",
"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "",
"You have unsaved changes are you sure you want to navigate away?": "Du har ikke arkivert endringene. Vil du fortsette uten å arkivere?",
"Restore last draft": "Gjenopprett siste utkast",
"Special character...": "Spesialtegn...",
"Special Character": "",
"Source code": "Kildekode",
"Insert/Edit code sample": "Sett inn / endre kodeeksempel",
"Language": "Språk",
"Code sample...": "Kodeeksempel",
"Left to right": "Venstre til høyre",
"Right to left": "Høyre til venstre",
"Title": "Tittel",
"Fullscreen": "Fullskjerm",
"Action": "Handling",
"Shortcut": "Snarvei",
"Help": "Hjelp",
"Address": "Adresse",
"Focus to menubar": "Fokus på menylinje",
"Focus to toolbar": "Fokus på verktøylinje",
"Focus to element path": "Fokus på elementsti",
"Focus to contextual toolbar": "Fokus på kontekstuell verktøylinje",
"Insert link (if link plugin activated)": "Sett inn lenke (dersom lenketillegg er aktivert)",
"Save (if save plugin activated)": "Lagre (dersom lagretillegg er aktivert)",
"Find (if searchreplace plugin activated)": "Finn (dersom tillegg for søk og erstatt er aktivert)",
"Plugins installed ({0}):": "Installerte tillegg ({0}):",
"Premium plugins:": "Premiumtillegg:",
"Learn more...": "Les mer ...",
"You are using {0}": "Du bruker {0}",
"Plugins": "Programtillegg",
"Handy Shortcuts": "Nyttige snarveier",
"Horizontal line": "Horisontal linje",
"Insert/edit image": "Sett inn / rediger bilde",
"Alternative description": "Alternativ beskrivelse",
"Accessibility": "Tilgjengelighet",
"Image is decorative": "Bilde er dekorasjon",
"Source": "Kilde",
"Dimensions": "Størrelser",
"Constrain proportions": "Begrens proporsjoner",
"General": "Generelt",
"Advanced": "Avansert",
"Style": "Stil",
"Vertical space": "Vertikal avstand",
"Horizontal space": "Horisontal avstand",
"Border": "Ramme",
"Insert image": "Sett inn bilde",
"Image...": "Bilde...",
"Image list": "Bildeliste",
"Resize": "Skaler",
"Insert date/time": "Sett inn dato/tid",
"Date/time": "Dato/tid",
"Insert/edit link": "Sett inn / rediger lenke",
"Text to display": "Tekst som skal vises",
"Url": "",
"Open link in...": "Åpne lenke i..",
"Current window": "Nåværende vindu",
"None": "Ingen",
"New window": "Nytt vindu",
"Open link": "Åpne lenke",
"Remove link": "Fjern lenke",
"Anchors": "Forankringspunkter",
"Link...": "Lenke...",
"Paste or type a link": "Lim inn eller skriv en lenke",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "Oppgitt URL ser ut til å være en e-postadresse. Ønsker du å sette inn påkrevet mailto: prefiks foran e-postadressen?",
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "URL du skrev inn ser ut som en ekstern adresse. Vil du legge til det obligatoriske prefikset http://?",
"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "Nettadressen du fylte inn ser ut til å være en ekstern. Ønsker du å legge til påkrevd 'https://'-prefiks?",
"Link list": "Liste over lenker",
"Insert video": "Sett inn video",
"Insert/edit video": "Sett inn / rediger video",
"Insert/edit media": "Sett inn / endre media",
"Alternative source": "Alternativ kilde",
"Alternative source URL": "Alternativ kilde URL",
"Media poster (Image URL)": "Mediaposter (bilde-URL)",
"Paste your embed code below:": "Lim inn inkluderingskoden nedenfor:",
"Embed": "Inkluder",
"Media...": "Media..",
"Nonbreaking space": "Hardt mellomrom",
"Page break": "Sideskifte",
"Paste as text": "Lim inn som tekst",
"Preview": "Forhåndsvis",
"Print": "",
"Print...": "Skriv ut...",
"Save": "Lagre",
"Find": "Søk etter",
"Replace with": "Erstatt med",
"Replace": "Erstatt",
"Replace all": "Erstatt alle",
"Previous": "Forrige",
"Next": "Neste",
"Find and Replace": "Finn og erstatt",
"Find and replace...": "Finn og erstatt...",
"Could not find the specified string.": "Kunne ikke finne den spesifiserte teksten",
"Match case": "Skill mellom store / små bokstaver",
"Find whole words only": "Finn kun hele ord",
"Find in selection": "Finn i utvalg",
"Insert table": "Sett inn tabell",
"Table properties": "Tabellegenskaper",
"Delete table": "Slett tabell",
"Cell": "Celle",
"Row": "Rad",
"Column": "Kolonne",
"Cell properties": "Celleegenskaper",
"Merge cells": "Slå sammen celler",
"Split cell": "Splitt celle",
"Insert row before": "Sett inn rad før",
"Insert row after": "Sett inn rad etter",
"Delete row": "Slett rad",
"Row properties": "Radegenskaper",
"Cut row": "Klipp ut rad",
"Cut column": "",
"Copy row": "Kopier rad",
"Copy column": "",
"Paste row before": "Lim inn rad før",
"Paste column before": "",
"Paste row after": "Lim inn rad etter",
"Paste column after": "",
"Insert column before": "Sett inn kolonne før",
"Insert column after": "Sett inn kolonne etter",
"Delete column": "Slett kolonne",
"Cols": "Kolonner",
"Rows": "Rader",
"Width": "Bredde",
"Height": "Høyde",
"Cell spacing": "Celleavstand",
"Cell padding": "Cellemarg",
"Row clipboard actions": "",
"Column clipboard actions": "",
"Table styles": "",
"Cell styles": "",
"Column header": "",
"Row header": "",
"Table caption": "",
"Caption": "Bildetekst",
"Show caption": "Vis bildetekst",
"Left": "Venstre",
"Center": "Senter",
"Right": "Høyre",
"Cell type": "Celletype",
"Scope": "Omfang",
"Alignment": "Justering",
"Horizontal align": "",
"Vertical align": "",
"Top": "Topp",
"Middle": "Sentrert",
"Bottom": "Bunn",
"Header cell": "Overskriftscelle",
"Row group": "Radgruppe",
"Column group": "Kolonnegruppe",
"Row type": "Radtype",
"Header": "",
"Body": "Brødtekst",
"Footer": "Bunntekst",
"Border color": "Rammefarge",
"Solid": "",
"Dotted": "",
"Dashed": "",
"Double": "",
"Groove": "",
"Ridge": "",
"Inset": "",
"Outset": "",
"Hidden": "",
"Insert template...": "Sett inn mal..",
"Templates": "Maler",
"Template": "Mal",
"Insert Template": "",
"Text color": "Tekstfarge",
"Background color": "Bakgrunnsfarge",
"Custom...": "Tilpasset...",
"Custom color": "Tilpasset farge",
"No color": "Ingen farge",
"Remove color": "Fjern farge",
"Show blocks": "Vis blokker",
"Show invisible characters": "Vis skjulte tegn",
"Word count": "Ordtelling",
"Count": "Opptelling",
"Document": "Dokument",
"Selection": "Utvalg",
"Words": "Ord",
"Words: {0}": "Ord: {0}",
"{0} words": "{0} ord",
"File": "Fil",
"Edit": "Rediger",
"Insert": "Sett inn",
"View": "Vis",
"Format": "",
"Table": "Tabell",
"Tools": "Verktøy",
"Powered by {0}": "Drevet av {0}",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Tekstredigering. Tast ALT-F9 for meny. Tast ALT-F10 for verktøylinje. Tast ALT-0 for hjelp.",
"Image title": "Bildetittel",
"Border width": "Bordbredde",
"Border style": "Bordstil",
"Error": "Feil",
"Warn": "Advarsel",
"Valid": "Gyldig",
"To open the popup, press Shift+Enter": "For å åpne popup, trykk Shift+Enter",
"Rich Text Area": "",
"Rich Text Area. Press ALT-0 for help.": "Rik-tekstområde. Trykk ALT-0 for hjelp.",
"System Font": "Systemfont",
"Failed to upload image: {0}": "Opplasting av bilde feilet: {0}",
"Failed to load plugin: {0} from url {1}": "Kunne ikke laste tillegg: {0} from url {1}",
"Failed to load plugin url: {0}": "Kunne ikke laste tillegg url: {0}",
"Failed to initialize plugin: {0}": "Kunne ikke initialisere tillegg: {0}",
"example": "eksempel",
"Search": "Søk",
"All": "Alle",
"Currency": "Valuta",
"Text": "Tekst",
"Quotations": "Sitater",
"Mathematical": "Matematisk",
"Extended Latin": "Utvidet latin",
"Symbols": "Symboler",
"Arrows": "Piler",
"User Defined": "Brukerdefinert",
"dollar sign": "dollartegn",
"currency sign": "valutasymbol",
"euro-currency sign": "Euro-valutasymbol",
"colon sign": "kolon-symbol",
"cruzeiro sign": "cruzeiro-symbol",
"french franc sign": "franske franc-symbol",
"lira sign": "lire-symbol",
"mill sign": "mill-symbol",
"naira sign": "naira-symbol",
"peseta sign": "peseta-symbol",
"rupee sign": "rupee-symbol",
"won sign": "won-symbol",
"new sheqel sign": "Ny sheqel-symbol",
"dong sign": "dong-symbol",
"kip sign": "kip-symbol",
"tugrik sign": "tugrik-symbol",
"drachma sign": "drachma-symbol",
"german penny symbol": "tysk penny-symbol",
"peso sign": "peso-symbol",
"guarani sign": "quarani-symbol",
"austral sign": "austral-symbol",
"hryvnia sign": "hryvina-symbol",
"cedi sign": "credi-symbol",
"livre tournois sign": "livre tournois-symbol",
"spesmilo sign": "spesmilo-symbol",
"tenge sign": "tenge-symbol",
"indian rupee sign": "indisk rupee-symbol",
"turkish lira sign": "tyrkisk lire-symbol",
"nordic mark sign": "nordisk mark-symbol",
"manat sign": "manat-symbol",
"ruble sign": "ruble-symbol",
"yen character": "yen-symbol",
"yuan character": "yuan-symbol",
"yuan character, in hong kong and taiwan": "yuan-symbol, i Hongkong og Taiwan",
"yen/yuan character variant one": "yen/yuan-symbol variant en",
"Emojis": "",
"Emojis...": "",
"Loading emojis...": "",
"Could not load emojis": "",
"People": "Mennesker",
"Animals and Nature": "Dyr og natur",
"Food and Drink": "Mat og drikke",
"Activity": "Aktivitet",
"Travel and Places": "Reise og steder",
"Objects": "Objekter",
"Flags": "Flagg",
"Characters": "Tegn",
"Characters (no spaces)": "Tegn (uten mellomrom)",
"{0} characters": "{0} tegn",
"Error: Form submit field collision.": "Feil: Skjemafelt innsendingskollisjon.",
"Error: No form element found.": "Feil: Intet skjemafelt funnet.",
"Color swatch": "Fargepalett",
"Color Picker": "Fargevelger",
"Invalid hex color code: {0}": "",
"Invalid input": "",
"R": "",
"Red component": "",
"G": "",
"Green component": "",
"B": "",
"Blue component": "",
"#": "",
"Hex color code": "",
"Range 0 to 255": "",
"Turquoise": "Turkis",
"Green": "Grønn",
"Blue": "Blå",
"Purple": "Lilla",
"Navy Blue": "Marineblå",
"Dark Turquoise": "Mørk turkis",
"Dark Green": "Mørkegrønn",
"Medium Blue": "Mellomblå",
"Medium Purple": "Medium lilla",
"Midnight Blue": "Midnattblå",
"Yellow": "Gul",
"Orange": "Oransje",
"Red": "Rød",
"Light Gray": "Lys grå",
"Gray": "Grå",
"Dark Yellow": "Mørk gul",
"Dark Orange": "Mørk oransje",
"Dark Red": "Mørkerød",
"Medium Gray": "Medium grå",
"Dark Gray": "Mørk grå",
"Light Green": "Lys grønn",
"Light Yellow": "Lys gul",
"Light Red": "Lys rød",
"Light Purple": "Lys lilla",
"Light Blue": "Lys blå",
"Dark Purple": "Mørk lilla",
"Dark Blue": "Mørk blå",
"Black": "Svart",
"White": "Hvit",
"Switch to or from fullscreen mode": "Bytt til eller fra fullskjermmodus",
"Open help dialog": "Åpne hjelp-dialog",
"history": "historikk",
"styles": "stiler",
"formatting": "formatering",
"alignment": "justering",
"indentation": "innrykk",
"Font": "Skrift",
"Size": "Størrelse",
"More...": "Mer...",
"Select...": "Velg...",
"Preferences": "Innstillinger",
"Yes": "Ja",
"No": "Nei",
"Keyboard Navigation": "Navigering med tastaturet",
"Version": "Versjon",
"Code view": "Kodevisning",
"Open popup menu for split buttons": "Åpne sprettoppmeny for splitt-knapper",
"List Properties": "Listeegenskaper",
"List properties...": "Listeegenskaper ...",
"Start list at number": "Start liste på nummer",
"Line height": "Linjehøyde",
"Dropped file type is not supported": "",
"Loading...": "",
"ImageProxy HTTP error: Rejected request": "",
"ImageProxy HTTP error: Could not find Image Proxy": "",
"ImageProxy HTTP error: Incorrect Image Proxy URL": "",
"ImageProxy HTTP error: Unknown ImageProxy error": ""
});

File diff suppressed because it is too large Load Diff

@ -503,6 +503,23 @@ $(function() {
} }
}); });
}); });
$("#metadata_backup").click(function() {
$("#DialogHeader").addClass("hidden");
$("#DialogFinished").addClass("hidden");
$("#DialogContent").html("");
$("#spinner2").show();
$.ajax({
method: "post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/metadata_backup",
success: function success(data) {
$("#spinner2").hide();
$("#DialogContent").html(data.text);
$("#DialogFinished").removeClass("hidden");
}
});
});
$("#perform_update").click(function() { $("#perform_update").click(function() {
$("#DialogHeader").removeClass("hidden"); $("#DialogHeader").removeClass("hidden");
$("#spinner2").show(); $("#spinner2").show();

@ -548,12 +548,6 @@ $(function() {
}, },
}); });
$("#user-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === "denied_column_value") {
confirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle);
}
});
$("#user-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table", $("#user-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) { function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter; var rows = rowsAfter;

@ -48,16 +48,12 @@ bookmark_label=Neno ma kombedi
tools.title=Gintic tools.title=Gintic
tools_label=Gintic tools_label=Gintic
first_page.title=Cit i pot buk mukwongo first_page.title=Cit i pot buk mukwongo
first_page.label=Cit i pot buk mukwongo
first_page_label=Cit i pot buk mukwongo first_page_label=Cit i pot buk mukwongo
last_page.title=Cit i pot buk magiko last_page.title=Cit i pot buk magiko
last_page.label=Cit i pot buk magiko
last_page_label=Cit i pot buk magiko last_page_label=Cit i pot buk magiko
page_rotate_cw.title=Wire i tung lacuc page_rotate_cw.title=Wire i tung lacuc
page_rotate_cw.label=Wire i tung lacuc
page_rotate_cw_label=Wire i tung lacuc page_rotate_cw_label=Wire i tung lacuc
page_rotate_ccw.title=Wire i tung lacam page_rotate_ccw.title=Wire i tung lacam
page_rotate_ccw.label=Wire i tung lacam
page_rotate_ccw_label=Wire i tung lacam page_rotate_ccw_label=Wire i tung lacam
cursor_text_select_tool.title=Cak gitic me yero coc cursor_text_select_tool.title=Cak gitic me yero coc
@ -124,7 +120,6 @@ print_progress_close=Juki
# (the _label strings are alt text for the buttons, the .title strings are # (the _label strings are alt text for the buttons, the .title strings are
# tooltips) # tooltips)
toggle_sidebar.title=Lok gintic ma inget toggle_sidebar.title=Lok gintic ma inget
toggle_sidebar_notification.title=Lok lanyut me nget (wiyewiye tye i gin acoya/attachments)
toggle_sidebar_label=Lok gintic ma inget toggle_sidebar_label=Lok gintic ma inget
document_outline.title=Nyut Wiyewiye me Gin acoya (dii-kiryo me yaro/kano jami weng) document_outline.title=Nyut Wiyewiye me Gin acoya (dii-kiryo me yaro/kano jami weng)
document_outline_label=Pek pa gin acoya document_outline_label=Pek pa gin acoya
@ -184,8 +179,6 @@ page_scale_actual=Dite kikome
# numerical scale value. # numerical scale value.
page_scale_percent={{scale}}% page_scale_percent={{scale}}%
# Loading indicator messages
loading_error_indicator=Bal
loading_error=Bal otime kun cano PDF. loading_error=Bal otime kun cano PDF.
invalid_file_error=Pwail me PDF ma pe atir onyo obale woko. invalid_file_error=Pwail me PDF ma pe atir onyo obale woko.
missing_file_error=Pwail me PDF tye ka rem. missing_file_error=Pwail me PDF tye ka rem.

@ -48,16 +48,12 @@ bookmark_label=Huidige aansig
tools.title=Nutsgoed tools.title=Nutsgoed
tools_label=Nutsgoed tools_label=Nutsgoed
first_page.title=Gaan na eerste bladsy first_page.title=Gaan na eerste bladsy
first_page.label=Gaan na eerste bladsy
first_page_label=Gaan na eerste bladsy first_page_label=Gaan na eerste bladsy
last_page.title=Gaan na laaste bladsy last_page.title=Gaan na laaste bladsy
last_page.label=Gaan na laaste bladsy
last_page_label=Gaan na laaste bladsy last_page_label=Gaan na laaste bladsy
page_rotate_cw.title=Roteer kloksgewys page_rotate_cw.title=Roteer kloksgewys
page_rotate_cw.label=Roteer kloksgewys
page_rotate_cw_label=Roteer kloksgewys page_rotate_cw_label=Roteer kloksgewys
page_rotate_ccw.title=Roteer anti-kloksgewys page_rotate_ccw.title=Roteer anti-kloksgewys
page_rotate_ccw.label=Roteer anti-kloksgewys
page_rotate_ccw_label=Roteer anti-kloksgewys page_rotate_ccw_label=Roteer anti-kloksgewys
cursor_text_select_tool.title=Aktiveer gereedskap om teks te merk cursor_text_select_tool.title=Aktiveer gereedskap om teks te merk
@ -101,7 +97,6 @@ print_progress_close=Kanselleer
# (the _label strings are alt text for the buttons, the .title strings are # (the _label strings are alt text for the buttons, the .title strings are
# tooltips) # tooltips)
toggle_sidebar.title=Sypaneel aan/af toggle_sidebar.title=Sypaneel aan/af
toggle_sidebar_notification.title=Sypaneel aan/af (dokument bevat skema/aanhegsels)
toggle_sidebar_label=Sypaneel aan/af toggle_sidebar_label=Sypaneel aan/af
document_outline.title=Wys dokumentskema (dubbelklik om alle items oop/toe te vou) document_outline.title=Wys dokumentskema (dubbelklik om alle items oop/toe te vou)
document_outline_label=Dokumentoorsig document_outline_label=Dokumentoorsig
@ -161,8 +156,6 @@ page_scale_actual=Werklike grootte
# numerical scale value. # numerical scale value.
page_scale_percent={{scale}}% page_scale_percent={{scale}}%
# Loading indicator messages
loading_error_indicator=Fout
loading_error='n Fout het voorgekom met die laai van die PDF. loading_error='n Fout het voorgekom met die laai van die PDF.
invalid_file_error=Ongeldige of korrupte PDF-lêer. invalid_file_error=Ongeldige of korrupte PDF-lêer.
missing_file_error=PDF-lêer is weg. missing_file_error=PDF-lêer is weg.

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

Loading…
Cancel
Save