mirror of
https://github.com/janeczku/calibre-web
synced 2025-10-29 22:33:01 +00:00
Merge branch 'master' into bulk-delete
This commit is contained in:
@@ -1,10 +1,3 @@
|
|||||||
# Short Notice from the maintainer
|
|
||||||
|
|
||||||
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
|
|
||||||
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
|
|
||||||
I have turned off all notifications from Github/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
|
|
||||||
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
|
|
||||||
|
|
||||||
# Calibre-Web
|
# Calibre-Web
|
||||||
|
|
||||||
Calibre-Web is a web app that offers a clean and intuitive interface for browsing, reading, and downloading eBooks using a valid [Calibre](https://calibre-ebook.com) database.
|
Calibre-Web is a web app that offers a clean and intuitive interface for browsing, reading, and downloading eBooks using a valid [Calibre](https://calibre-ebook.com) database.
|
||||||
|
|||||||
75
SECURITY.md
75
SECURITY.md
@@ -10,41 +10,46 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
|
|||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
| Fixed in | Description |CVE number |
|
| Fixed in | Description |CVE number |
|
||||||
|---------------|--------------------------------------------------------------------------------------------------------------------|---------|
|
|---------------|--------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||||
| 3rd July 2018 | Guest access acts as a backdoor ||
|
| 3rd July 2018 | Guest access acts as a backdoor ||
|
||||||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||||
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||||
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
||||||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
||||||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
||||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
||||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
||||||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
||||||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
||||||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
||||||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
||||||
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 |CVE-2021-4164|
|
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 |CVE-2021-4164|
|
||||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 |CVE-2021-4170|
|
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 |CVE-2021-4170|
|
||||||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
||||||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
||||||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
||||||
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz |CVE-2022-0352|
|
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz |CVE-2022-0352|
|
||||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 |CVE-2022-0339|
|
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 |CVE-2022-0339|
|
||||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon |CVE-2022-0273|
|
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon |CVE-2022-0273|
|
||||||
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
|
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon |CVE-2022-0405|
|
||||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
|
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 |CVE-2022-0767|
|
||||||
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
|
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH |CVE-2022-0766|
|
||||||
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
|
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) |CVE-2022-30765|
|
||||||
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
|
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 |CVE-2022-0939|
|
||||||
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|
|
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley |CVE-2022-0990|
|
||||||
| V 0.6.20 | Credentials for emails are now stored encrypted ||
|
| V 0.6.20 | Credentials for emails are now stored encrypted ||
|
||||||
| V 0.6.20 | Login is rate limited ||
|
| V 0.6.20 | Login is rate limited ||
|
||||||
| V 0.6.20 | Passwordstrength can be forced ||
|
| V 0.6.20 | Passwordstrength can be forced ||
|
||||||
| V 0.6.21 | SMTP server credentials are no longer returned to client ||
|
| V 0.6.21 | SMTP server credentials are no longer returned to client ||
|
||||||
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible ||
|
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible ||
|
||||||
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser ||
|
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser ||
|
||||||
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
||||||
|
| V 0.6.22 | Upload mimetype is checked to prevent malicious file content in the books library ||
|
||||||
|
| V 0.6.22 | Cross-site scripting (XSS) stored in comments section is prevented better (switching from lxml to bleach for sanitizing strings) ||
|
||||||
|
| V 0.6.23 | Cookies are no longer stored for opds basic authentication and proxy authentication ||
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Statement regarding Log4j (CVE-2021-44228 and related)
|
## Statement regarding Log4j (CVE-2021-44228 and related)
|
||||||
|
|||||||
9
cps/__init__.py
Executable file → Normal file
9
cps/__init__.py
Executable file → Normal file
@@ -72,6 +72,9 @@ mimetypes.add_type('application/mpeg', '.mpeg')
|
|||||||
mimetypes.add_type('audio/mpeg', '.mp3')
|
mimetypes.add_type('audio/mpeg', '.mp3')
|
||||||
mimetypes.add_type('audio/x-m4a', '.m4a')
|
mimetypes.add_type('audio/x-m4a', '.m4a')
|
||||||
mimetypes.add_type('audio/x-m4a', '.m4b')
|
mimetypes.add_type('audio/x-m4a', '.m4b')
|
||||||
|
mimetypes.add_type('audio/x-hx-aac-adts', '.aac')
|
||||||
|
mimetypes.add_type('audio/vnd.dolby.dd-raw', '.ac3')
|
||||||
|
mimetypes.add_type('video/x-ms-asf', '.asf')
|
||||||
mimetypes.add_type('audio/ogg', '.ogg')
|
mimetypes.add_type('audio/ogg', '.ogg')
|
||||||
mimetypes.add_type('application/ogg', '.oga')
|
mimetypes.add_type('application/ogg', '.oga')
|
||||||
mimetypes.add_type('text/css', '.css')
|
mimetypes.add_type('text/css', '.css')
|
||||||
@@ -84,8 +87,10 @@ app = Flask(__name__)
|
|||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Strict',
|
SESSION_COOKIE_SAMESITE='Strict',
|
||||||
REMEMBER_COOKIE_SAMESITE='Strict', # will be available in flask-login 0.5.1 earliest
|
REMEMBER_COOKIE_SAMESITE='Strict',
|
||||||
WTF_CSRF_SSL_STRICT=False
|
WTF_CSRF_SSL_STRICT=False,
|
||||||
|
SESSION_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "session",
|
||||||
|
REMEMBER_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "remember_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
lm = MyLoginManager()
|
lm = MyLoginManager()
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import importlib
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import jinja2
|
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, constants, dep_check
|
from . import db, calibre_db, converter, uploader, constants, dep_check
|
||||||
@@ -41,8 +41,11 @@ req = dep_check.load_dependencies(False)
|
|||||||
opt = dep_check.load_dependencies(True)
|
opt = dep_check.load_dependencies(True)
|
||||||
for i in (req + opt):
|
for i in (req + opt):
|
||||||
modules[i[1]] = i[0]
|
modules[i[1]] = i[0]
|
||||||
modules['Jinja2'] = jinja2.__version__
|
modules['Jinja2'] = importlib.metadata.version("jinja2")
|
||||||
modules['pySqlite'] = sqlite3.version
|
try:
|
||||||
|
modules['pySqlite'] = sqlite3.version
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
modules['SQLite'] = sqlite3.sqlite_version
|
modules['SQLite'] = sqlite3.sqlite_version
|
||||||
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
|
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
|
||||||
|
|
||||||
|
|||||||
33
cps/admin.py
Executable file → Normal file
33
cps/admin.py
Executable file → Normal file
@@ -54,6 +54,7 @@ from .services.worker import WorkerThread
|
|||||||
from .usermanagement import user_login_required
|
from .usermanagement import user_login_required
|
||||||
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
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -463,9 +464,9 @@ def edit_list_user(param):
|
|||||||
if 'value[]' in vals:
|
if 'value[]' in vals:
|
||||||
setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]']))
|
setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]']))
|
||||||
else:
|
else:
|
||||||
setattr(user, param, vals['value'].strip())
|
setattr(user, param, strip_whitespaces(vals['value']))
|
||||||
else:
|
else:
|
||||||
vals['value'] = vals['value'].strip()
|
vals['value'] = strip_whitespaces(vals['value'])
|
||||||
if param == 'name':
|
if param == 'name':
|
||||||
if user.name == "Guest":
|
if user.name == "Guest":
|
||||||
raise Exception(_("Guest Name can't be changed"))
|
raise Exception(_("Guest Name can't be changed"))
|
||||||
@@ -566,7 +567,7 @@ def update_view_configuration():
|
|||||||
_config_string(to_save, "config_calibre_web_title")
|
_config_string(to_save, "config_calibre_web_title")
|
||||||
_config_string(to_save, "config_columns_to_ignore")
|
_config_string(to_save, "config_columns_to_ignore")
|
||||||
if _config_string(to_save, "config_title_regex"):
|
if _config_string(to_save, "config_title_regex"):
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.create_functions(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(_("Invalid Read Column"), category="error")
|
flash(_("Invalid Read Column"), category="error")
|
||||||
@@ -690,7 +691,7 @@ def delete_domain():
|
|||||||
def list_domain(allow):
|
def list_domain(allow):
|
||||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
answer = ub.session.query(ub.Registration).filter(ub.Registration.allow == allow).all()
|
||||||
json_dumps = json.dumps([{"domain": r.domain.replace('%', '*').replace('_', '?'), "id": r.id} for r in answer])
|
json_dumps = json.dumps([{"domain": r.domain.replace('%', '*').replace('_', '?'), "id": r.id} for r in answer])
|
||||||
js = json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"')
|
js = json.dumps(json_dumps.replace('"', "'")).strip('"')
|
||||||
response = make_response(js.replace("'", '"'))
|
response = make_response(js.replace("'", '"'))
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
@@ -1100,7 +1101,7 @@ def _config_checkbox_int(to_save, x):
|
|||||||
|
|
||||||
|
|
||||||
def _config_string(to_save, x):
|
def _config_string(to_save, x):
|
||||||
return config.set_from_dictionary(to_save, x, lambda y: y.strip().strip(u'\u200B\u200C\u200D\ufeff') if y else y)
|
return config.set_from_dictionary(to_save, x, lambda y: strip_whitespaces(y) if y else y)
|
||||||
|
|
||||||
|
|
||||||
def _configuration_gdrive_helper(to_save):
|
def _configuration_gdrive_helper(to_save):
|
||||||
@@ -1311,9 +1312,9 @@ def update_mailsettings():
|
|||||||
if to_save.get("mail_password_e", ""):
|
if to_save.get("mail_password_e", ""):
|
||||||
_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.mail_server = to_save.get('mail_server', "").strip()
|
config.mail_server = strip_whitespaces(to_save.get('mail_server', ""))
|
||||||
config.mail_from = to_save.get('mail_from', "").strip()
|
config.mail_from = strip_whitespaces(to_save.get('mail_from', ""))
|
||||||
config.mail_login = to_save.get('mail_login', "").strip()
|
config.mail_login = strip_whitespaces(to_save.get('mail_login', ""))
|
||||||
try:
|
try:
|
||||||
config.save()
|
config.save()
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
@@ -1678,10 +1679,10 @@ def cancel_task():
|
|||||||
def _db_simulate_change():
|
def _db_simulate_change():
|
||||||
param = request.form.to_dict()
|
param = request.form.to_dict()
|
||||||
to_save = dict()
|
to_save = dict()
|
||||||
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
|
to_save['config_calibre_dir'] = strip_whitespaces(re.sub(r'[\\/]metadata\.db$',
|
||||||
'',
|
'',
|
||||||
param['config_calibre_dir'],
|
param['config_calibre_dir'],
|
||||||
flags=re.IGNORECASE).strip()
|
flags=re.IGNORECASE))
|
||||||
db_valid, db_change = calibre_db.check_valid_db(to_save["config_calibre_dir"],
|
db_valid, db_change = calibre_db.check_valid_db(to_save["config_calibre_dir"],
|
||||||
ub.app_DB_path,
|
ub.app_DB_path,
|
||||||
config.config_calibre_uuid)
|
config.config_calibre_uuid)
|
||||||
@@ -1715,6 +1716,13 @@ def _db_configuration_update_helper():
|
|||||||
db_change = True
|
db_change = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
return _db_configuration_result('{}'.format(ex), gdrive_error)
|
||||||
|
config.config_calibre_split = to_save.get('config_calibre_split', 0) == "on"
|
||||||
|
if config.config_calibre_split:
|
||||||
|
split_dir = to_save.get("config_calibre_split_dir")
|
||||||
|
if not os.path.exists(split_dir):
|
||||||
|
return _db_configuration_result(_("Books path not valid"), gdrive_error)
|
||||||
|
else:
|
||||||
|
_config_string(to_save, "config_calibre_split_dir")
|
||||||
|
|
||||||
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"]:
|
||||||
@@ -1740,8 +1748,6 @@ def _db_configuration_update_helper():
|
|||||||
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(_("DB is not Writeable"), category="warning")
|
flash(_("DB is not Writeable"), category="warning")
|
||||||
_config_string(to_save, "config_calibre_split_dir")
|
|
||||||
config.config_calibre_split = to_save.get('config_calibre_split', 0) == "on"
|
|
||||||
calibre_db.update_config(config)
|
calibre_db.update_config(config)
|
||||||
config.save()
|
config.save()
|
||||||
return _db_configuration_result(None, gdrive_error)
|
return _db_configuration_result(None, gdrive_error)
|
||||||
@@ -1775,9 +1781,8 @@ def _configuration_update_helper():
|
|||||||
|
|
||||||
if "config_upload_formats" in to_save:
|
if "config_upload_formats" in to_save:
|
||||||
to_save["config_upload_formats"] = ','.join(
|
to_save["config_upload_formats"] = ','.join(
|
||||||
helper.uniq([x.lstrip().rstrip().lower() for x in to_save["config_upload_formats"].split(',')]))
|
helper.uniq([x.strip().lower() for x in to_save["config_upload_formats"].split(',')]))
|
||||||
_config_string(to_save, "config_upload_formats")
|
_config_string(to_save, "config_upload_formats")
|
||||||
# constants.EXTENSIONS_UPLOAD = config.config_upload_formats.split(',')
|
|
||||||
|
|
||||||
_config_string(to_save, "config_calibre")
|
_config_string(to_save, "config_calibre")
|
||||||
_config_string(to_save, "config_binariesdir")
|
_config_string(to_save, "config_binariesdir")
|
||||||
|
|||||||
153
cps/audio.py
Normal file
153
cps/audio.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2024 Ozzieisaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import mutagen
|
||||||
|
import base64
|
||||||
|
from . import cover, logger
|
||||||
|
|
||||||
|
from cps.constants import BookMeta
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
def get_audio_file_info(tmp_file_path, original_file_extension, original_file_name, no_cover_processing):
|
||||||
|
tmp_cover_name = None
|
||||||
|
audio_file = mutagen.File(tmp_file_path)
|
||||||
|
comments = None
|
||||||
|
if original_file_extension in [".mp3", ".wav", ".aiff"]:
|
||||||
|
cover_data = list()
|
||||||
|
for key, val in audio_file.tags.items():
|
||||||
|
if key.startswith("APIC:"):
|
||||||
|
cover_data.append(val)
|
||||||
|
if key.startswith("COMM:"):
|
||||||
|
comments = val.text[0]
|
||||||
|
title = audio_file.tags.get('TIT2').text[0] if "TIT2" in audio_file.tags else None
|
||||||
|
author = audio_file.tags.get('TPE1').text[0] if "TPE1" in audio_file.tags else None
|
||||||
|
if author is None:
|
||||||
|
author = audio_file.tags.get('TPE2').text[0] if "TPE2" in audio_file.tags else None
|
||||||
|
tags = audio_file.tags.get('TCON').text[0] if "TCON" in audio_file.tags else None # Genre
|
||||||
|
series = audio_file.tags.get('TALB').text[0] if "TALB" in audio_file.tags else None# Album
|
||||||
|
series_id = audio_file.tags.get('TRCK').text[0] if "TRCK" in audio_file.tags else None # track no.
|
||||||
|
publisher = audio_file.tags.get('TPUB').text[0] if "TPUB" in audio_file.tags else None
|
||||||
|
pubdate = str(audio_file.tags.get('TDRL').text[0]) if "TDRL" in audio_file.tags else None
|
||||||
|
if not pubdate:
|
||||||
|
pubdate = str(audio_file.tags.get('TDRC').text[0]) if "TDRC" in audio_file.tags else None
|
||||||
|
if not pubdate:
|
||||||
|
pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None
|
||||||
|
if cover_data and not no_cover_processing:
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
cover_info = cover_data[0]
|
||||||
|
for dat in cover_data:
|
||||||
|
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
|
||||||
|
cover_info = dat
|
||||||
|
break
|
||||||
|
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||||
|
elif original_file_extension in [".ogg", ".flac", ".opus", ".ogv"]:
|
||||||
|
title = audio_file.tags.get('TITLE')[0] if "TITLE" in audio_file else None
|
||||||
|
author = audio_file.tags.get('ARTIST')[0] if "ARTIST" in audio_file else None
|
||||||
|
comments = audio_file.tags.get('COMMENTS')[0] if "COMMENTS" in audio_file else None
|
||||||
|
tags = audio_file.tags.get('GENRE')[0] if "GENRE" in audio_file else None # Genre
|
||||||
|
series = audio_file.tags.get('ALBUM')[0] if "ALBUM" in audio_file else None
|
||||||
|
series_id = audio_file.tags.get('TRACKNUMBER')[0] if "TRACKNUMBER" in audio_file else None
|
||||||
|
publisher = audio_file.tags.get('LABEL')[0] if "LABEL" in audio_file else None
|
||||||
|
pubdate = audio_file.tags.get('DATE')[0] if "DATE" in audio_file else None
|
||||||
|
cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE')
|
||||||
|
if not no_cover_processing:
|
||||||
|
if cover_data:
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0]))
|
||||||
|
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||||
|
if hasattr(audio_file, "pictures"):
|
||||||
|
cover_info = audio_file.pictures[0]
|
||||||
|
for dat in audio_file.pictures:
|
||||||
|
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
|
||||||
|
cover_info = dat
|
||||||
|
break
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
|
||||||
|
elif original_file_extension in [".aac"]:
|
||||||
|
title = audio_file.tags.get('Title').value if "Title" in audio_file else None
|
||||||
|
author = audio_file.tags.get('Artist').value if "Artist" in audio_file else None
|
||||||
|
comments = audio_file.tags.get('Comment').value if "Comment" in audio_file else None
|
||||||
|
tags = audio_file.tags.get('Genre').value if "Genre" in audio_file else None
|
||||||
|
series = audio_file.tags.get('Album').value if "Album" in audio_file else None
|
||||||
|
series_id = audio_file.tags.get('Track').value if "Track" in audio_file else None
|
||||||
|
publisher = audio_file.tags.get('Label').value if "Label" in audio_file else None
|
||||||
|
pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None
|
||||||
|
cover_data = audio_file.tags['Cover Art (Front)']
|
||||||
|
if cover_data and not no_cover_processing:
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
with open(tmp_cover_name, "wb") as cover_file:
|
||||||
|
cover_file.write(cover_data.value.split(b"\x00",1)[1])
|
||||||
|
elif original_file_extension in [".asf"]:
|
||||||
|
title = audio_file.tags.get('Title')[0].value if "Title" in audio_file else None
|
||||||
|
author = audio_file.tags.get('Artist')[0].value if "Artist" in audio_file else None
|
||||||
|
comments = audio_file.tags.get('Comments')[0].value if "Comments" in audio_file else None
|
||||||
|
tags = audio_file.tags.get('Genre')[0].value if "Genre" in audio_file else None
|
||||||
|
series = audio_file.tags.get('Album')[0].value if "Album" in audio_file else None
|
||||||
|
series_id = audio_file.tags.get('Track')[0].value if "Track" in audio_file else None
|
||||||
|
publisher = audio_file.tags.get('Label')[0].value if "Label" in audio_file else None
|
||||||
|
pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None
|
||||||
|
cover_data = audio_file.tags.get('WM/Picture', None)
|
||||||
|
if cover_data and not no_cover_processing:
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
with open(tmp_cover_name, "wb") as cover_file:
|
||||||
|
cover_file.write(cover_data[0].value)
|
||||||
|
elif original_file_extension in [".mp4", ".m4a", ".m4b"]:
|
||||||
|
title = audio_file.tags.get('©nam')[0] if "©nam" in audio_file.tags else None
|
||||||
|
author = audio_file.tags.get('©ART')[0] if "©ART" in audio_file.tags else None
|
||||||
|
comments = audio_file.tags.get('©cmt')[0] if "©cmt" in audio_file.tags else None
|
||||||
|
tags = audio_file.tags.get('©gen')[0] if "©gen" in audio_file.tags else None
|
||||||
|
series = audio_file.tags.get('©alb')[0] if "©alb" in audio_file.tags else None
|
||||||
|
series_id = str(audio_file.tags.get('trkn')[0][0]) if "trkn" in audio_file.tags else None
|
||||||
|
publisher = ""
|
||||||
|
pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None
|
||||||
|
cover_data = audio_file.tags.get('covr', None)
|
||||||
|
if cover_data and not no_cover_processing:
|
||||||
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
|
||||||
|
cover_type = None
|
||||||
|
for c in cover_data:
|
||||||
|
if c.imageformat == mutagen.mp4.AtomDataType.JPEG:
|
||||||
|
cover_type =".jpg"
|
||||||
|
cover_bin = c
|
||||||
|
break
|
||||||
|
elif c.imageformat == mutagen.mp4.AtomDataType.PNG:
|
||||||
|
cover_type = ".png"
|
||||||
|
cover_bin = c
|
||||||
|
break
|
||||||
|
if cover_type:
|
||||||
|
cover.cover_processing(tmp_file_path, cover_bin, cover_type)
|
||||||
|
else:
|
||||||
|
logger.error("Unknown covertype in file {} ".format(original_file_name))
|
||||||
|
|
||||||
|
return BookMeta(
|
||||||
|
file_path=tmp_file_path,
|
||||||
|
extension=original_file_extension,
|
||||||
|
title=title or original_file_name ,
|
||||||
|
author="Unknown" if author is None else author,
|
||||||
|
cover=tmp_cover_name,
|
||||||
|
description="" if comments is None else comments,
|
||||||
|
tags="" if tags is None else tags,
|
||||||
|
series="" if series is None else series,
|
||||||
|
series_id="1" if series_id is None else series_id.split("/")[0],
|
||||||
|
languages="",
|
||||||
|
publisher= "" if publisher is None else publisher,
|
||||||
|
pubdate="" if pubdate is None else pubdate,
|
||||||
|
identifiers=[],
|
||||||
|
)
|
||||||
15
cps/comic.py
15
cps/comic.py
@@ -130,7 +130,7 @@ def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
|||||||
return cover.cover_processing(tmp_file_name, cover_data, extension)
|
return cover.cover_processing(tmp_file_name, cover_data, extension)
|
||||||
|
|
||||||
|
|
||||||
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable):
|
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable, no_cover_processing):
|
||||||
if use_comic_meta:
|
if use_comic_meta:
|
||||||
try:
|
try:
|
||||||
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
|
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
|
||||||
@@ -155,14 +155,17 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
|
|
||||||
lang = loaded_metadata.language or ""
|
lang = loaded_metadata.language or ""
|
||||||
loaded_metadata.language = isoLanguages.get_lang3(lang)
|
loaded_metadata.language = isoLanguages.get_lang3(lang)
|
||||||
|
if not no_cover_processing:
|
||||||
|
cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable)
|
||||||
|
else:
|
||||||
|
cover_file = None
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=loaded_metadata.title or original_file_name,
|
title=loaded_metadata.title or original_file_name,
|
||||||
author=" & ".join([credit["person"]
|
author=" & ".join([credit["person"]
|
||||||
for credit in loaded_metadata.credits if credit["role"] == "Writer"]) or 'Unknown',
|
for credit in loaded_metadata.credits if credit["role"] == "Writer"]) or 'Unknown',
|
||||||
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
|
cover=cover_file,
|
||||||
description=loaded_metadata.comments or "",
|
description=loaded_metadata.comments or "",
|
||||||
tags="",
|
tags="",
|
||||||
series=loaded_metadata.series or "",
|
series=loaded_metadata.series or "",
|
||||||
@@ -171,13 +174,17 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
publisher="",
|
publisher="",
|
||||||
pubdate="",
|
pubdate="",
|
||||||
identifiers=[])
|
identifiers=[])
|
||||||
|
if not no_cover_processing:
|
||||||
|
cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable)
|
||||||
|
else:
|
||||||
|
cover_file = None
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
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='Unknown',
|
author='Unknown',
|
||||||
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
|
cover=cover_file,
|
||||||
description="",
|
description="",
|
||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ except ImportError:
|
|||||||
|
|
||||||
from . import constants, logger
|
from . import constants, logger
|
||||||
from .subproc_wrapper import process_wait
|
from .subproc_wrapper import process_wait
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
_Base = declarative_base()
|
_Base = declarative_base()
|
||||||
@@ -182,26 +182,6 @@ class _Settings(_Base):
|
|||||||
class ConfigSQL(object):
|
class ConfigSQL(object):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
'''self.config_calibre_uuid = None
|
|
||||||
self.config_calibre_split_dir = None
|
|
||||||
self.dirty = None
|
|
||||||
self.config_logfile = None
|
|
||||||
self.config_upload_formats = None
|
|
||||||
self.mail_gmail_token = None
|
|
||||||
self.mail_server_type = None
|
|
||||||
self.mail_server = None
|
|
||||||
self.config_log_level = None
|
|
||||||
self.config_allowed_column_value = None
|
|
||||||
self.config_denied_column_value = None
|
|
||||||
self.config_allowed_tags = None
|
|
||||||
self.config_denied_tags = None
|
|
||||||
self.config_default_show = None
|
|
||||||
self.config_default_role = None
|
|
||||||
self.config_keyfile = None
|
|
||||||
self.config_certfile = None
|
|
||||||
self.config_rarfile_location = None
|
|
||||||
self.config_kepubifypath = None
|
|
||||||
self.config_binariesdir = None'''
|
|
||||||
self.__dict__["dirty"] = list()
|
self.__dict__["dirty"] = list()
|
||||||
|
|
||||||
def init_config(self, session, secret_key, cli):
|
def init_config(self, session, secret_key, cli):
|
||||||
@@ -288,19 +268,19 @@ class ConfigSQL(object):
|
|||||||
|
|
||||||
def list_denied_tags(self):
|
def list_denied_tags(self):
|
||||||
mct = self.config_denied_tags or ""
|
mct = self.config_denied_tags or ""
|
||||||
return [t.strip() for t in mct.split(",")]
|
return [strip_whitespaces(t) for t in mct.split(",")]
|
||||||
|
|
||||||
def list_allowed_tags(self):
|
def list_allowed_tags(self):
|
||||||
mct = self.config_allowed_tags or ""
|
mct = self.config_allowed_tags or ""
|
||||||
return [t.strip() for t in mct.split(",")]
|
return [strip_whitespaces(t) for t in mct.split(",")]
|
||||||
|
|
||||||
def list_denied_column_values(self):
|
def list_denied_column_values(self):
|
||||||
mct = self.config_denied_column_value or ""
|
mct = self.config_denied_column_value or ""
|
||||||
return [t.strip() for t in mct.split(",")]
|
return [strip_whitespaces(t) for t in mct.split(",")]
|
||||||
|
|
||||||
def list_allowed_column_values(self):
|
def list_allowed_column_values(self):
|
||||||
mct = self.config_allowed_column_value or ""
|
mct = self.config_allowed_column_value or ""
|
||||||
return [t.strip() for t in mct.split(",")]
|
return [strip_whitespaces(t) for t in mct.split(",")]
|
||||||
|
|
||||||
def get_log_level(self):
|
def get_log_level(self):
|
||||||
return logger.get_level_name(self.config_log_level)
|
return logger.get_level_name(self.config_log_level)
|
||||||
@@ -372,7 +352,7 @@ class ConfigSQL(object):
|
|||||||
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
|
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
|
||||||
have_metadata_db = os.path.isfile(db_file)
|
have_metadata_db = os.path.isfile(db_file)
|
||||||
self.db_configured = have_metadata_db
|
self.db_configured = have_metadata_db
|
||||||
# constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
|
|
||||||
from . import cli_param
|
from . import cli_param
|
||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
|
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
|
|||||||
'series_id, languages, publisher, pubdate, identifiers')
|
'series_id, languages, publisher, pubdate, identifiers')
|
||||||
|
|
||||||
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||||
STABLE_VERSION = {'version': '0.6.23b'}
|
STABLE_VERSION = {'version': '0.6.24b'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = dict()
|
NIGHTLY_VERSION = dict()
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
@@ -193,7 +193,7 @@ THUMBNAIL_TYPE_AUTHOR = 3
|
|||||||
COVER_THUMBNAIL_ORIGINAL = 0
|
COVER_THUMBNAIL_ORIGINAL = 0
|
||||||
COVER_THUMBNAIL_SMALL = 1
|
COVER_THUMBNAIL_SMALL = 1
|
||||||
COVER_THUMBNAIL_MEDIUM = 2
|
COVER_THUMBNAIL_MEDIUM = 2
|
||||||
COVER_THUMBNAIL_LARGE = 3
|
COVER_THUMBNAIL_LARGE = 4
|
||||||
|
|
||||||
# clean-up the module namespace
|
# clean-up the module namespace
|
||||||
del sys, os, namedtuple
|
del sys, os, namedtuple
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from datetime import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
@@ -496,7 +497,7 @@ class LoginManager:
|
|||||||
duration = timedelta(seconds=duration)
|
duration = timedelta(seconds=duration)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
expires = datetime.utcnow() + duration
|
expires = datetime.now(timezone.utc) + duration
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
||||||
|
|||||||
44
cps/db.py
44
cps/db.py
@@ -20,10 +20,11 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import unidecode
|
import unidecode
|
||||||
from weakref import WeakSet
|
from weakref import WeakSet
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -48,7 +49,7 @@ from flask import flash
|
|||||||
|
|
||||||
from . import logger, ub, isoLanguages
|
from . import logger, ub, isoLanguages
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -378,10 +379,10 @@ class Books(Base):
|
|||||||
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
|
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
|
||||||
sort = Column(String(collation='NOCASE'))
|
sort = Column(String(collation='NOCASE'))
|
||||||
author_sort = Column(String(collation='NOCASE'))
|
author_sort = Column(String(collation='NOCASE'))
|
||||||
timestamp = Column(TIMESTAMP, default=datetime.utcnow)
|
timestamp = Column(TIMESTAMP, default=lambda: datetime.now(timezone.utc))
|
||||||
pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE)
|
pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE)
|
||||||
series_index = Column(String, nullable=False, default="1.0")
|
series_index = Column(String, nullable=False, default="1.0")
|
||||||
last_modified = Column(TIMESTAMP, default=datetime.utcnow)
|
last_modified = Column(TIMESTAMP, default=lambda: datetime.now(timezone.utc))
|
||||||
path = Column(String, default="", nullable=False)
|
path = Column(String, default="", nullable=False)
|
||||||
has_cover = Column(Integer, default=0)
|
has_cover = Column(Integer, default=0)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
@@ -533,7 +534,7 @@ class CalibreDB:
|
|||||||
def init_session(self, expire_on_commit=True):
|
def init_session(self, expire_on_commit=True):
|
||||||
self.session = self.session_factory()
|
self.session = self.session_factory()
|
||||||
self.session.expire_on_commit = expire_on_commit
|
self.session.expire_on_commit = expire_on_commit
|
||||||
self.update_title_sort(self.config)
|
self.create_functions(self.config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_db_cc_classes(cls, cc):
|
def setup_db_cc_classes(cls, cc):
|
||||||
@@ -875,10 +876,11 @@ class CalibreDB:
|
|||||||
authors_ordered = list()
|
authors_ordered = list()
|
||||||
# error = False
|
# error = False
|
||||||
for auth in sort_authors:
|
for auth in sort_authors:
|
||||||
results = self.session.query(Authors).filter(Authors.sort == auth.lstrip().strip()).all()
|
auth = strip_whitespaces(auth)
|
||||||
|
results = self.session.query(Authors).filter(Authors.sort == auth).all()
|
||||||
# ToDo: How to handle not found author name
|
# ToDo: How to handle not found author name
|
||||||
if not len(results):
|
if not len(results):
|
||||||
log.error("Author {} not found to display name in right order".format(auth.strip()))
|
log.error("Author {} not found to display name in right order".format(auth))
|
||||||
# error = True
|
# error = True
|
||||||
break
|
break
|
||||||
for r in results:
|
for r in results:
|
||||||
@@ -900,7 +902,8 @@ class CalibreDB:
|
|||||||
|
|
||||||
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
||||||
query = query or ''
|
query = query or ''
|
||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.create_functions()
|
||||||
|
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
entries = self.session.query(database).filter(tag_filter). \
|
entries = self.session.query(database).filter(tag_filter). \
|
||||||
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||||
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
||||||
@@ -908,7 +911,8 @@ class CalibreDB:
|
|||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
def check_exists_book(self, authr, title):
|
def check_exists_book(self, authr, title):
|
||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.create_functions()
|
||||||
|
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
q = list()
|
q = list()
|
||||||
author_terms = re.split(r'\s*&\s*', authr)
|
author_terms = re.split(r'\s*&\s*', authr)
|
||||||
for author_term in author_terms:
|
for author_term in author_terms:
|
||||||
@@ -918,8 +922,9 @@ class CalibreDB:
|
|||||||
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
||||||
|
|
||||||
def search_query(self, term, config, *join):
|
def search_query(self, term, config, *join):
|
||||||
term.strip().lower()
|
strip_whitespaces(term).lower()
|
||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.create_functions()
|
||||||
|
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
q = list()
|
q = list()
|
||||||
author_terms = re.split("[, ]+", term)
|
author_terms = re.split("[, ]+", term)
|
||||||
for author_term in author_terms:
|
for author_term in author_terms:
|
||||||
@@ -1017,7 +1022,7 @@ class CalibreDB:
|
|||||||
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
|
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
|
||||||
|
|
||||||
def update_title_sort(self, config, conn=None):
|
def create_functions(self, config=None):
|
||||||
# user defined sort function for calibre databases (Series, etc.)
|
# user defined sort function for calibre databases (Series, etc.)
|
||||||
def _title_sort(title):
|
def _title_sort(title):
|
||||||
# calibre sort stuff
|
# calibre sort stuff
|
||||||
@@ -1026,16 +1031,19 @@ class CalibreDB:
|
|||||||
if match:
|
if match:
|
||||||
prep = match.group(1)
|
prep = match.group(1)
|
||||||
title = title[len(prep):] + ', ' + prep
|
title = title[len(prep):] + ', ' + prep
|
||||||
return title.strip()
|
return strip_whitespaces(title)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# sqlalchemy <1.4.24
|
# sqlalchemy <1.4.24 and sqlalchemy 2.0
|
||||||
conn = conn or self.session.connection().connection.driver_connection
|
conn = self.session.connection().connection.driver_connection
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# sqlalchemy >1.4.24 and sqlalchemy 2.0
|
# sqlalchemy >1.4.24
|
||||||
conn = conn or self.session.connection().connection.connection
|
conn = self.session.connection().connection.connection
|
||||||
try:
|
try:
|
||||||
conn.create_function("title_sort", 1, _title_sort)
|
if config:
|
||||||
|
conn.create_function("title_sort", 1, _title_sort)
|
||||||
|
conn.create_function('uuid4', 0, lambda: str(uuid4()))
|
||||||
|
conn.create_function("lower", 1, lcase)
|
||||||
except sqliteOperationalError:
|
except sqliteOperationalError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from flask_babel.speaklater import LazyString
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import send_file, __version__
|
from flask import send_file
|
||||||
|
import importlib
|
||||||
|
|
||||||
from . import logger, config
|
from . import logger, config
|
||||||
from .about import collect_stats
|
from .about import collect_stats
|
||||||
@@ -49,7 +50,8 @@ def assemble_logfiles(file_name):
|
|||||||
with open(f, 'rb') as fd:
|
with open(f, 'rb') as fd:
|
||||||
shutil.copyfileobj(fd, wfd)
|
shutil.copyfileobj(fd, wfd)
|
||||||
wfd.seek(0)
|
wfd.seek(0)
|
||||||
if int(__version__.split('.')[0]) < 2:
|
version = importlib.metadata.version("flask")
|
||||||
|
if int(version.split('.')[0]) < 2:
|
||||||
return send_file(wfd,
|
return send_file(wfd,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename=os.path.basename(file_name))
|
attachment_filename=os.path.basename(file_name))
|
||||||
@@ -72,7 +74,8 @@ def send_debug():
|
|||||||
for fp in file_list:
|
for fp in file_list:
|
||||||
zf.write(fp, os.path.basename(fp))
|
zf.write(fp, os.path.basename(fp))
|
||||||
memory_zip.seek(0)
|
memory_zip.seek(0)
|
||||||
if int(__version__.split('.')[0]) < 2:
|
version = importlib.metadata.version("flask")
|
||||||
|
if int(version.split('.')[0]) < 2:
|
||||||
return send_file(memory_zip,
|
return send_file(memory_zip,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename="Calibre-Web-debug-pack.zip")
|
attachment_filename="Calibre-Web-debug-pack.zip")
|
||||||
|
|||||||
594
cps/editbooks.py
594
cps/editbooks.py
@@ -21,10 +21,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
import json
|
import json
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
|
||||||
from markupsafe import escape, Markup # dependency of flask
|
from markupsafe import escape, Markup # dependency of flask
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ from .kobo_sync_status import change_archived_books
|
|||||||
from .redirect import get_redirect_location
|
from .redirect import get_redirect_location
|
||||||
from .file_helper import validate_mime_type
|
from .file_helper import validate_mime_type
|
||||||
from .usermanagement import user_login_required, login_required_if_no_ano
|
from .usermanagement import user_login_required, login_required_if_no_ano
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
editbook = Blueprint('edit-book', __name__)
|
editbook = Blueprint('edit-book', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@@ -97,157 +97,22 @@ def show_edit_book(book_id):
|
|||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
def edit_book(book_id):
|
def edit_book(book_id):
|
||||||
modify_date = False
|
return do_edit_book(book_id)
|
||||||
edit_error = False
|
|
||||||
|
|
||||||
# create the function for sorting...
|
|
||||||
calibre_db.update_title_sort(config)
|
|
||||||
|
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
|
||||||
# Book not found
|
|
||||||
if not book:
|
|
||||||
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
|
||||||
category="error")
|
|
||||||
return redirect(url_for("web.index"))
|
|
||||||
|
|
||||||
to_save = request.form.to_dict()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Update folder of book on local disk
|
|
||||||
edited_books_id = None
|
|
||||||
title_author_error = None
|
|
||||||
# handle book title change
|
|
||||||
title_change = handle_title_on_edit(book, to_save["book_title"])
|
|
||||||
# handle book author change
|
|
||||||
input_authors, author_change = handle_author_on_edit(book, to_save["author_name"])
|
|
||||||
if author_change or title_change:
|
|
||||||
edited_books_id = book.id
|
|
||||||
modify_date = True
|
|
||||||
title_author_error = helper.update_dir_structure(edited_books_id,
|
|
||||||
config.get_book_path(),
|
|
||||||
input_authors[0])
|
|
||||||
if title_author_error:
|
|
||||||
flash(title_author_error, category="error")
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
|
||||||
|
|
||||||
# handle upload other formats from local disk
|
|
||||||
meta = upload_single_file(request, book, book_id)
|
|
||||||
# only merge metadata if file was uploaded and no error occurred (meta equals not false or none)
|
|
||||||
upload_format = False
|
|
||||||
if meta:
|
|
||||||
upload_format = merge_metadata(to_save, meta)
|
|
||||||
# handle upload covers from local disk
|
|
||||||
cover_upload_success = upload_cover(request, book)
|
|
||||||
if cover_upload_success:
|
|
||||||
book.has_cover = 1
|
|
||||||
modify_date = True
|
|
||||||
|
|
||||||
# upload new covers or new file formats to google drive
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
|
||||||
|
|
||||||
if to_save.get("cover_url", None):
|
|
||||||
if not current_user.role_upload():
|
|
||||||
edit_error = True
|
|
||||||
flash(_("User has no rights to upload cover"), category="error")
|
|
||||||
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
|
|
||||||
book.has_cover = 0
|
|
||||||
else:
|
|
||||||
result, error = helper.save_cover_from_url(to_save["cover_url"].strip(), book.path)
|
|
||||||
if result is True:
|
|
||||||
book.has_cover = 1
|
|
||||||
modify_date = True
|
|
||||||
helper.replace_cover_thumbnail_cache(book.id)
|
|
||||||
else:
|
|
||||||
flash(error, category="error")
|
|
||||||
|
|
||||||
# Add default series_index to book
|
|
||||||
modify_date |= edit_book_series_index(to_save["series_index"], book)
|
|
||||||
# Handle book comments/description
|
|
||||||
modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
|
|
||||||
# Handle identifiers
|
|
||||||
input_identifiers = identifier_list(to_save, book)
|
|
||||||
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
|
||||||
if warning:
|
|
||||||
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
|
|
||||||
modify_date |= modification
|
|
||||||
# Handle book tags
|
|
||||||
modify_date |= edit_book_tags(to_save['tags'], book)
|
|
||||||
# Handle book series
|
|
||||||
modify_date |= edit_book_series(to_save["series"], book)
|
|
||||||
# handle book publisher
|
|
||||||
modify_date |= edit_book_publisher(to_save['publisher'], book)
|
|
||||||
# handle book languages
|
|
||||||
try:
|
|
||||||
modify_date |= edit_book_languages(to_save['languages'], book, upload_format)
|
|
||||||
except ValueError as e:
|
|
||||||
flash(str(e), category="error")
|
|
||||||
edit_error = True
|
|
||||||
# handle book ratings
|
|
||||||
modify_date |= edit_book_ratings(to_save, book)
|
|
||||||
# handle cc data
|
|
||||||
modify_date |= edit_all_cc_data(book_id, book, to_save)
|
|
||||||
|
|
||||||
if to_save.get("pubdate", None):
|
|
||||||
try:
|
|
||||||
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
|
|
||||||
except ValueError as e:
|
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
|
||||||
flash(str(e), category="error")
|
|
||||||
edit_error = True
|
|
||||||
else:
|
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
|
||||||
|
|
||||||
if modify_date:
|
|
||||||
book.last_modified = datetime.utcnow()
|
|
||||||
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.commit()
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
|
||||||
if meta is not False \
|
|
||||||
and edit_error is not True \
|
|
||||||
and title_author_error is not True \
|
|
||||||
and cover_upload_success is not False:
|
|
||||||
flash(_("Metadata successfully updated"), category="success")
|
|
||||||
if "detail_view" in to_save:
|
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
|
||||||
else:
|
|
||||||
return render_edit_book(book_id)
|
|
||||||
except ValueError as e:
|
|
||||||
log.error_or_exception("Error: {}".format(e))
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
flash(str(e), category="error")
|
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
|
||||||
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
|
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error")
|
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
|
||||||
except Exception as ex:
|
|
||||||
log.error_or_exception(ex)
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
flash(_("Error editing book: {}".format(ex)), category="error")
|
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/upload", methods=["POST"])
|
@editbook.route("/upload", methods=["POST"])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@upload_required
|
@upload_required
|
||||||
def upload():
|
def upload():
|
||||||
if not config.config_uploading:
|
if len(request.files.getlist("btn-upload-format")):
|
||||||
abort(404)
|
book_id = request.form.get('book_id', -1)
|
||||||
if request.method == 'POST' and 'btn-upload' in request.files:
|
return do_edit_book(book_id, request.files.getlist("btn-upload-format"))
|
||||||
|
elif len(request.files.getlist("btn-upload")):
|
||||||
for requested_file in request.files.getlist("btn-upload"):
|
for requested_file in request.files.getlist("btn-upload"):
|
||||||
try:
|
try:
|
||||||
modify_date = False
|
modify_date = False
|
||||||
# create the function for sorting...
|
# create the function for sorting...
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.create_functions(config)
|
||||||
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
|
||||||
|
|
||||||
meta, error = file_handling_on_upload(requested_file)
|
meta, error = file_handling_on_upload(requested_file)
|
||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
@@ -275,9 +140,7 @@ def upload():
|
|||||||
input_authors[0],
|
input_authors[0],
|
||||||
meta.file_path,
|
meta.file_path,
|
||||||
title_dir + meta.extension.lower())
|
title_dir + meta.extension.lower())
|
||||||
|
|
||||||
move_coverfile(meta, db_book)
|
move_coverfile(meta, db_book)
|
||||||
|
|
||||||
if modify_date:
|
if modify_date:
|
||||||
calibre_db.set_metadata_dirty(book_id)
|
calibre_db.set_metadata_dirty(book_id)
|
||||||
# save data to database, reread data
|
# save data to database, reread data
|
||||||
@@ -305,6 +168,7 @@ def upload():
|
|||||||
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
||||||
category="error")
|
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')
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
|
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
|
||||||
@@ -508,7 +372,7 @@ def edit_book_param(param, vals):
|
|||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
else:
|
else:
|
||||||
return _("Parameter not found"), 400
|
return _("Parameter not found"), 400
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.now(timezone.utc)
|
||||||
|
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
# revert change for sort if automatic fields link is deactivated
|
# revert change for sort if automatic fields link is deactivated
|
||||||
@@ -681,7 +545,7 @@ def table_xchange_author_title():
|
|||||||
# toDo: Handle error
|
# toDo: Handle error
|
||||||
edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0])
|
edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0])
|
||||||
if modify_date:
|
if modify_date:
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.now(timezone.utc)
|
||||||
calibre_db.set_metadata_dirty(book.id)
|
calibre_db.set_metadata_dirty(book.id)
|
||||||
try:
|
try:
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
@@ -696,23 +560,166 @@ def table_xchange_author_title():
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def merge_metadata(to_save, meta):
|
def do_edit_book(book_id, upload_formats=None):
|
||||||
if to_save.get('author_name', "") == _('Unknown'):
|
modify_date = False
|
||||||
to_save['author_name'] = ''
|
edit_error = False
|
||||||
if to_save.get('book_title', "") == _('Unknown'):
|
|
||||||
to_save['book_title'] = ''
|
# create the function for sorting...
|
||||||
if not to_save["languages"] and meta.languages:
|
calibre_db.create_functions(config)
|
||||||
upload_language = True
|
|
||||||
else:
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
upload_language = False
|
# Book not found
|
||||||
|
if not book:
|
||||||
|
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||||
|
category="error")
|
||||||
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
|
to_save = request.form.to_dict()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Update folder of book on local disk
|
||||||
|
edited_books_id = None
|
||||||
|
title_author_error = None
|
||||||
|
# upload_mode = False
|
||||||
|
# handle book title change
|
||||||
|
if "title" in to_save:
|
||||||
|
title_change = handle_title_on_edit(book, to_save["title"])
|
||||||
|
# handle book author change
|
||||||
|
if not upload_formats:
|
||||||
|
input_authors, author_change = handle_author_on_edit(book, to_save["authors"])
|
||||||
|
if author_change or title_change:
|
||||||
|
edited_books_id = book.id
|
||||||
|
modify_date = True
|
||||||
|
title_author_error = helper.update_dir_structure(edited_books_id,
|
||||||
|
config.get_book_path(),
|
||||||
|
input_authors[0])
|
||||||
|
if title_author_error:
|
||||||
|
flash(title_author_error, category="error")
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
|
|
||||||
|
# handle book ratings
|
||||||
|
modify_date |= edit_book_ratings(to_save, book)
|
||||||
|
else:
|
||||||
|
# handle upload other formats from local disk
|
||||||
|
to_save, edit_error = upload_book_formats(upload_formats, book, book_id, book.has_cover)
|
||||||
|
# handle upload covers from local disk
|
||||||
|
cover_upload_success = upload_cover(request, book)
|
||||||
|
if cover_upload_success or to_save.get("format_cover"):
|
||||||
|
book.has_cover = 1
|
||||||
|
modify_date = True
|
||||||
|
|
||||||
|
# upload new covers or new file formats to google drive
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
|
||||||
|
if to_save.get("cover_url",):
|
||||||
|
if not current_user.role_upload():
|
||||||
|
edit_error = True
|
||||||
|
flash(_("User has no rights to upload cover"), category="error")
|
||||||
|
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
|
||||||
|
book.has_cover = 0
|
||||||
|
else:
|
||||||
|
result, error = helper.save_cover_from_url(to_save["cover_url"].strip(), book.path)
|
||||||
|
if result is True:
|
||||||
|
book.has_cover = 1
|
||||||
|
modify_date = True
|
||||||
|
helper.replace_cover_thumbnail_cache(book.id)
|
||||||
|
else:
|
||||||
|
flash(error, category="error")
|
||||||
|
|
||||||
|
# Add default series_index to book
|
||||||
|
modify_date |= edit_book_series_index(to_save.get("series_index"), book)
|
||||||
|
# Handle book comments/description
|
||||||
|
modify_date |= edit_book_comments(Markup(to_save.get('comments')).unescape(), book)
|
||||||
|
# Handle identifiers
|
||||||
|
input_identifiers = identifier_list(to_save, book)
|
||||||
|
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
||||||
|
if warning:
|
||||||
|
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
|
||||||
|
modify_date |= modification
|
||||||
|
# Handle book tags
|
||||||
|
modify_date |= edit_book_tags(to_save.get('tags'), book)
|
||||||
|
# Handle book series
|
||||||
|
modify_date |= edit_book_series(to_save.get("series"), book)
|
||||||
|
# handle book publisher
|
||||||
|
modify_date |= edit_book_publisher(to_save.get('publisher'), book)
|
||||||
|
# handle book languages
|
||||||
|
try:
|
||||||
|
invalid = []
|
||||||
|
modify_date |= edit_book_languages(to_save.get('languages'), book, upload_mode=upload_formats,
|
||||||
|
invalid=invalid)
|
||||||
|
if invalid:
|
||||||
|
for lang in invalid:
|
||||||
|
flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning")
|
||||||
|
except ValueError as e:
|
||||||
|
flash(str(e), category="error")
|
||||||
|
edit_error = True
|
||||||
|
# handle cc data
|
||||||
|
modify_date |= edit_all_cc_data(book_id, book, to_save)
|
||||||
|
|
||||||
|
if to_save.get("pubdate") is not None:
|
||||||
|
if to_save.get("pubdate"):
|
||||||
|
try:
|
||||||
|
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
|
||||||
|
except ValueError as e:
|
||||||
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
|
flash(str(e), category="error")
|
||||||
|
edit_error = True
|
||||||
|
else:
|
||||||
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
|
if modify_date:
|
||||||
|
book.last_modified = datetime.now(timezone.utc)
|
||||||
|
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.commit()
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
if edit_error is not True and title_author_error is not True and cover_upload_success is not False:
|
||||||
|
flash(_("Metadata successfully updated"), category="success")
|
||||||
|
|
||||||
|
if upload_formats:
|
||||||
|
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
|
||||||
|
return Response(json.dumps(resp), mimetype='application/json')
|
||||||
|
|
||||||
|
if "detail_view" in to_save:
|
||||||
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
else:
|
||||||
|
return render_edit_book(book_id)
|
||||||
|
except ValueError as e:
|
||||||
|
log.error_or_exception("Error: {}".format(e))
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
flash(str(e), category="error")
|
||||||
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
|
||||||
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error")
|
||||||
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
except Exception as ex:
|
||||||
|
log.error_or_exception(ex)
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
flash(_("Error editing book: {}".format(ex)), category="error")
|
||||||
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
|
||||||
|
|
||||||
|
def merge_metadata(book, meta, to_save):
|
||||||
|
if meta.cover:
|
||||||
|
to_save['cover_format'] = meta.cover
|
||||||
for s_field, m_field in [
|
for s_field, m_field in [
|
||||||
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
|
('tags', 'tags'), ('authors', 'author'), ('series', 'series'),
|
||||||
('series_index', 'series_id'), ('languages', 'languages'),
|
('series_index', 'series_id'), ('languages', 'languages'),
|
||||||
('book_title', 'title')]:
|
('title', 'title'), ('comments', 'description')]:
|
||||||
to_save[s_field] = to_save[s_field] or getattr(meta, m_field, '')
|
try:
|
||||||
to_save["description"] = to_save["description"] or Markup(
|
val = None if len(getattr(book, s_field)) else getattr(meta, m_field, '')
|
||||||
getattr(meta, 'description', '')).unescape()
|
except TypeError:
|
||||||
return upload_language
|
val = None if len(str(getattr(book, s_field))) else getattr(meta, m_field, '')
|
||||||
|
if val:
|
||||||
|
to_save[s_field] = val
|
||||||
|
|
||||||
|
|
||||||
def identifier_list(to_save, book):
|
def identifier_list(to_save, book):
|
||||||
"""Generate a list of Identifiers from form information"""
|
"""Generate a list of Identifiers from form information"""
|
||||||
@@ -832,8 +839,8 @@ def create_book_on_upload(modify_date, meta):
|
|||||||
pubdate = datetime(101, 1, 1)
|
pubdate = datetime(101, 1, 1)
|
||||||
|
|
||||||
# Calibre adds books with utc as timezone
|
# Calibre adds books with utc as timezone
|
||||||
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), pubdate,
|
db_book = db.Books(title, "", sort_authors, datetime.now(timezone.utc), pubdate,
|
||||||
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
'1', datetime.now(timezone.utc), path, meta.cover, db_author, [], "")
|
||||||
|
|
||||||
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
|
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
|
||||||
'author')
|
'author')
|
||||||
@@ -1100,7 +1107,7 @@ def render_edit_book(book_id):
|
|||||||
|
|
||||||
def edit_book_ratings(to_save, book):
|
def edit_book_ratings(to_save, book):
|
||||||
changed = False
|
changed = False
|
||||||
if to_save.get("rating", "").strip():
|
if strip_whitespaces(to_save.get("rating", "")):
|
||||||
old_rating = False
|
old_rating = False
|
||||||
if len(book.ratings) > 0:
|
if len(book.ratings) > 0:
|
||||||
old_rating = book.ratings[0].rating
|
old_rating = book.ratings[0].rating
|
||||||
@@ -1123,84 +1130,93 @@ def edit_book_ratings(to_save, book):
|
|||||||
|
|
||||||
|
|
||||||
def edit_book_tags(tags, book):
|
def edit_book_tags(tags, book):
|
||||||
input_tags = tags.split(',')
|
if tags is not None:
|
||||||
input_tags = list(map(lambda it: it.strip(), input_tags))
|
input_tags = tags.split(',')
|
||||||
# Remove duplicates
|
input_tags = list(map(lambda it: strip_whitespaces(it), input_tags))
|
||||||
input_tags = helper.uniq(input_tags)
|
# Remove duplicates
|
||||||
return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags')
|
input_tags = helper.uniq(input_tags)
|
||||||
|
return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags')
|
||||||
|
return False
|
||||||
|
|
||||||
def edit_book_series(series, book):
|
def edit_book_series(series, book):
|
||||||
input_series = [series.strip()]
|
if series is not None:
|
||||||
input_series = [x for x in input_series if x != '']
|
input_series = [strip_whitespaces(series)]
|
||||||
return modify_database_object(input_series, book.series, db.Series, calibre_db.session, 'series')
|
input_series = [x for x in input_series if x != '']
|
||||||
|
return modify_database_object(input_series, book.series, db.Series, calibre_db.session, 'series')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def edit_book_series_index(series_index, book):
|
def edit_book_series_index(series_index, book):
|
||||||
# Add default series_index to book
|
if series_index:
|
||||||
modify_date = False
|
# Add default series_index to book
|
||||||
series_index = series_index or '1'
|
modify_date = False
|
||||||
if not series_index.replace('.', '', 1).isdigit():
|
series_index = series_index or '1'
|
||||||
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
|
if not series_index.replace('.', '', 1).isdigit():
|
||||||
return False
|
flash(_("Seriesindex: %(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
|
||||||
if str(book.series_index) != series_index:
|
return False
|
||||||
book.series_index = series_index
|
if str(book.series_index) != series_index:
|
||||||
modify_date = True
|
book.series_index = series_index
|
||||||
return modify_date
|
modify_date = True
|
||||||
|
return modify_date
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
def edit_book_comments(comments, book):
|
def edit_book_comments(comments, book):
|
||||||
modify_date = False
|
if comments is not None:
|
||||||
if comments:
|
modify_date = False
|
||||||
comments = clean_string(comments, book.id)
|
|
||||||
if len(book.comments):
|
|
||||||
if book.comments[0].text != comments:
|
|
||||||
book.comments[0].text = comments
|
|
||||||
modify_date = True
|
|
||||||
else:
|
|
||||||
if comments:
|
if comments:
|
||||||
book.comments.append(db.Comments(comment=comments, book=book.id))
|
comments = clean_string(comments, book.id)
|
||||||
modify_date = True
|
if len(book.comments):
|
||||||
return modify_date
|
if book.comments[0].text != comments:
|
||||||
|
book.comments[0].text = comments
|
||||||
|
modify_date = True
|
||||||
|
else:
|
||||||
|
if comments:
|
||||||
|
book.comments.append(db.Comments(comment=comments, book=book.id))
|
||||||
|
modify_date = True
|
||||||
|
return modify_date
|
||||||
|
|
||||||
|
|
||||||
def edit_book_languages(languages, book, upload_mode=False, invalid=None):
|
def edit_book_languages(languages, book, upload_mode=False, invalid=None):
|
||||||
input_languages = languages.split(',')
|
if languages is not None:
|
||||||
unknown_languages = []
|
input_languages = languages.split(',')
|
||||||
if not upload_mode:
|
unknown_languages = []
|
||||||
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages)
|
if not upload_mode:
|
||||||
else:
|
input_l = isoLanguages.get_language_code_from_name(get_locale(), input_languages, unknown_languages)
|
||||||
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
|
||||||
for lang in unknown_languages:
|
|
||||||
log.error("'%s' is not a valid language", lang)
|
|
||||||
if isinstance(invalid, list):
|
|
||||||
invalid.append(lang)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
|
input_l = isoLanguages.get_valid_language_codes_from_code(get_locale(), input_languages, unknown_languages)
|
||||||
# ToDo: Not working correct
|
for lang in unknown_languages:
|
||||||
if upload_mode and len(input_l) == 1:
|
log.error("'%s' is not a valid language", lang)
|
||||||
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
if isinstance(invalid, list):
|
||||||
# the book it's language is set to the filter language
|
invalid.append(lang)
|
||||||
if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all":
|
else:
|
||||||
input_l[0] = calibre_db.session.query(db.Languages). \
|
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
|
||||||
filter(db.Languages.lang_code == current_user.filter_language()).first().lang_code
|
# ToDo: Not working correct
|
||||||
# Remove duplicates
|
if upload_mode and len(input_l) == 1:
|
||||||
input_l = helper.uniq(input_l)
|
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
||||||
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
# the book it's language is set to the filter language
|
||||||
|
if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all":
|
||||||
|
input_l[0] = calibre_db.session.query(db.Languages). \
|
||||||
|
filter(db.Languages.lang_code == current_user.filter_language()).first().lang_code
|
||||||
|
# Remove duplicates from normalized langcodes
|
||||||
|
input_l = helper.uniq(input_l)
|
||||||
|
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def edit_book_publisher(publishers, book):
|
def edit_book_publisher(publishers, book):
|
||||||
changed = False
|
if publishers is not None:
|
||||||
if publishers:
|
changed = False
|
||||||
publisher = publishers.rstrip().strip()
|
if publishers:
|
||||||
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
publisher = strip_whitespaces(publishers)
|
||||||
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
||||||
'publisher')
|
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
||||||
elif len(book.publishers):
|
'publisher')
|
||||||
changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher')
|
elif len(book.publishers):
|
||||||
return changed
|
changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher')
|
||||||
|
return changed
|
||||||
|
return False
|
||||||
|
|
||||||
def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
||||||
changed = False
|
changed = False
|
||||||
@@ -1240,7 +1256,7 @@ def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
|||||||
changed = False
|
changed = False
|
||||||
if c.datatype == 'rating':
|
if c.datatype == 'rating':
|
||||||
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
||||||
if to_save[cc_string].strip() != cc_db_value:
|
if strip_whitespaces(to_save[cc_string]) != cc_db_value:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
# remove old cc_val
|
# remove old cc_val
|
||||||
del_cc = getattr(book, cc_string)[0]
|
del_cc = getattr(book, cc_string)[0]
|
||||||
@@ -1250,15 +1266,15 @@ def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
|||||||
changed = True
|
changed = True
|
||||||
cc_class = db.cc_classes[c.id]
|
cc_class = db.cc_classes[c.id]
|
||||||
new_cc = calibre_db.session.query(cc_class).filter(
|
new_cc = calibre_db.session.query(cc_class).filter(
|
||||||
cc_class.value == to_save[cc_string].strip()).first()
|
cc_class.value == strip_whitespaces(to_save[cc_string])).first()
|
||||||
# if no cc val is found add it
|
# if no cc val is found add it
|
||||||
if new_cc is None:
|
if new_cc is None:
|
||||||
new_cc = cc_class(value=to_save[cc_string].strip())
|
new_cc = cc_class(value=strip_whitespaces(to_save[cc_string]))
|
||||||
calibre_db.session.add(new_cc)
|
calibre_db.session.add(new_cc)
|
||||||
changed = True
|
changed = True
|
||||||
calibre_db.session.flush()
|
calibre_db.session.flush()
|
||||||
new_cc = calibre_db.session.query(cc_class).filter(
|
new_cc = calibre_db.session.query(cc_class).filter(
|
||||||
cc_class.value == to_save[cc_string].strip()).first()
|
cc_class.value == strip_whitespaces(to_save[cc_string])).first()
|
||||||
# add cc value to book
|
# add cc value to book
|
||||||
getattr(book, cc_string).append(new_cc)
|
getattr(book, cc_string).append(new_cc)
|
||||||
return changed, to_save
|
return changed, to_save
|
||||||
@@ -1281,61 +1297,66 @@ def edit_cc_data(book_id, book, to_save, cc):
|
|||||||
changed = False
|
changed = False
|
||||||
for c in cc:
|
for c in cc:
|
||||||
cc_string = "custom_column_" + str(c.id)
|
cc_string = "custom_column_" + str(c.id)
|
||||||
if not c.is_multiple:
|
if to_save.get(cc_string) is not None:
|
||||||
if len(getattr(book, cc_string)) > 0:
|
if not c.is_multiple:
|
||||||
cc_db_value = getattr(book, cc_string)[0].value
|
if len(getattr(book, cc_string)) > 0:
|
||||||
else:
|
cc_db_value = getattr(book, cc_string)[0].value
|
||||||
cc_db_value = None
|
|
||||||
if to_save[cc_string].strip():
|
|
||||||
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
|
||||||
change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
|
||||||
else:
|
else:
|
||||||
change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
cc_db_value = None
|
||||||
changed |= change
|
if strip_whitespaces(to_save[cc_string]):
|
||||||
|
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
||||||
|
change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
||||||
|
else:
|
||||||
|
change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
||||||
|
changed |= change
|
||||||
|
else:
|
||||||
|
if cc_db_value is not None:
|
||||||
|
# remove old cc_val
|
||||||
|
del_cc = getattr(book, cc_string)[0]
|
||||||
|
getattr(book, cc_string).remove(del_cc)
|
||||||
|
if not del_cc.books or len(del_cc.books) == 0:
|
||||||
|
calibre_db.session.delete(del_cc)
|
||||||
|
changed = True
|
||||||
else:
|
else:
|
||||||
if cc_db_value is not None:
|
input_tags = to_save[cc_string].split(',')
|
||||||
# remove old cc_val
|
input_tags = list(map(lambda it: strip_whitespaces(it), input_tags))
|
||||||
del_cc = getattr(book, cc_string)[0]
|
changed |= modify_database_object(input_tags,
|
||||||
getattr(book, cc_string).remove(del_cc)
|
getattr(book, cc_string),
|
||||||
if not del_cc.books or len(del_cc.books) == 0:
|
db.cc_classes[c.id],
|
||||||
calibre_db.session.delete(del_cc)
|
calibre_db.session,
|
||||||
changed = True
|
'custom')
|
||||||
else:
|
|
||||||
input_tags = to_save[cc_string].split(',')
|
|
||||||
input_tags = list(map(lambda it: it.strip(), input_tags))
|
|
||||||
changed |= modify_database_object(input_tags,
|
|
||||||
getattr(book, cc_string),
|
|
||||||
db.cc_classes[c.id],
|
|
||||||
calibre_db.session,
|
|
||||||
'custom')
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
# returns None if no file is uploaded
|
# returns False if an error occurs or no book is uploaded, in all other cases the ebook metadata to change is returned
|
||||||
# returns False if an error occurs, in all other cases the ebook metadata is returned
|
def upload_book_formats(requested_files, book, book_id, no_cover=True):
|
||||||
def upload_single_file(file_request, book, book_id):
|
|
||||||
# Check and handle Uploaded file
|
# Check and handle Uploaded file
|
||||||
requested_file = file_request.files.get('btn-upload-format', None)
|
to_save = dict()
|
||||||
|
error = False
|
||||||
allowed_extensions = config.config_upload_formats.split(',')
|
allowed_extensions = config.config_upload_formats.split(',')
|
||||||
if requested_file:
|
for requested_file in requested_files:
|
||||||
|
current_filename = requested_file.filename
|
||||||
if config.config_check_extensions and allowed_extensions != ['']:
|
if config.config_check_extensions and allowed_extensions != ['']:
|
||||||
if not validate_mime_type(requested_file, allowed_extensions):
|
if not validate_mime_type(requested_file, allowed_extensions):
|
||||||
flash(_("File type isn't allowed to be uploaded to this server"), category="error")
|
flash(_("File type isn't allowed to be uploaded to this server"), category="error")
|
||||||
return False
|
error = True
|
||||||
# check for empty request
|
continue
|
||||||
if requested_file.filename != '':
|
if current_filename != '':
|
||||||
if not current_user.role_upload():
|
if not current_user.role_upload():
|
||||||
flash(_("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
|
error = True
|
||||||
if '.' in requested_file.filename:
|
continue
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
if '.' in current_filename:
|
||||||
|
file_ext = current_filename.rsplit('.', 1)[-1].lower()
|
||||||
if file_ext not in allowed_extensions and '' not in allowed_extensions:
|
if file_ext not in allowed_extensions and '' not in allowed_extensions:
|
||||||
flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext),
|
flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext),
|
||||||
category="error")
|
category="error")
|
||||||
return False
|
error = True
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
flash(_('File to be uploaded must have an extension'), category="error")
|
flash(_('File to be uploaded must have an extension'), category="error")
|
||||||
return False
|
error = True
|
||||||
|
continue
|
||||||
|
|
||||||
file_name = book.path.rsplit('/', 1)[-1]
|
file_name = book.path.rsplit('/', 1)[-1]
|
||||||
filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path))
|
filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path))
|
||||||
@@ -1348,41 +1369,50 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
except OSError:
|
except OSError:
|
||||||
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath),
|
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath),
|
||||||
category="error")
|
category="error")
|
||||||
return False
|
error = True
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
requested_file.save(saved_filename)
|
requested_file.save(saved_filename)
|
||||||
except OSError:
|
except OSError:
|
||||||
flash(_("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
|
error = True
|
||||||
|
continue
|
||||||
|
|
||||||
file_size = os.path.getsize(saved_filename)
|
file_size = os.path.getsize(saved_filename)
|
||||||
is_format = calibre_db.get_book_format(book_id, file_ext.upper())
|
|
||||||
|
|
||||||
# Format entry already exists, no need to update the database
|
# Format entry already exists, no need to update the database
|
||||||
if is_format:
|
if calibre_db.get_book_format(book_id, file_ext.upper()):
|
||||||
log.warning('Book format %s already existing', file_ext.upper())
|
log.warning('Book format %s already existing', file_ext.upper())
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
|
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
|
||||||
calibre_db.session.add(db_format)
|
calibre_db.session.add(db_format)
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.create_functions(config)
|
||||||
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(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
||||||
category="error")
|
category="error")
|
||||||
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
error = True
|
||||||
|
continue
|
||||||
|
|
||||||
# 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_("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)))
|
||||||
|
meta = uploader.process(
|
||||||
return uploader.process(
|
saved_filename,
|
||||||
saved_filename, *os.path.splitext(requested_file.filename),
|
*os.path.splitext(current_filename),
|
||||||
rar_executable=config.config_rarfile_location)
|
rar_executable=config.config_rarfile_location,
|
||||||
return None
|
no_cover=no_cover)
|
||||||
|
merge_metadata(book, meta, to_save)
|
||||||
|
#if to_save.get('languages'):
|
||||||
|
# langs = []
|
||||||
|
# for lang_code in to_save['languages'].split(','):
|
||||||
|
# langs.append(isoLanguages.get_language_name(get_locale(), lang_code))
|
||||||
|
# to_save['languages'] = ",".join(langs)
|
||||||
|
return to_save, error
|
||||||
|
|
||||||
|
|
||||||
def upload_cover(cover_request, book):
|
def upload_cover(cover_request, book):
|
||||||
@@ -1405,7 +1435,7 @@ def upload_cover(cover_request, book):
|
|||||||
|
|
||||||
def handle_title_on_edit(book, book_title):
|
def handle_title_on_edit(book, book_title):
|
||||||
# handle book title
|
# handle book title
|
||||||
book_title = book_title.rstrip().strip()
|
book_title = strip_whitespaces(book_title)
|
||||||
if book.title != book_title:
|
if book.title != book_title:
|
||||||
if book_title == '':
|
if book_title == '':
|
||||||
book_title = _(u'Unknown')
|
book_title = _(u'Unknown')
|
||||||
@@ -1416,7 +1446,6 @@ def handle_title_on_edit(book, book_title):
|
|||||||
|
|
||||||
def handle_author_on_edit(book, author_name, update_stored=True):
|
def handle_author_on_edit(book, author_name, update_stored=True):
|
||||||
change = False
|
change = False
|
||||||
# handle author(s)
|
|
||||||
input_authors = prepare_authors(author_name, config.get_book_path(), config.config_use_google_drive)
|
input_authors = prepare_authors(author_name, config.get_book_path(), config.config_use_google_drive)
|
||||||
|
|
||||||
# Search for each author if author is in database, if not, author name and sorted author name is generated new
|
# Search for each author if author is in database, if not, author name and sorted author name is generated new
|
||||||
@@ -1446,7 +1475,6 @@ def search_objects_remove(db_book_object, db_type, input_elements):
|
|||||||
if db_type == 'custom':
|
if db_type == 'custom':
|
||||||
type_elements = c_elements.value
|
type_elements = c_elements.value
|
||||||
else:
|
else:
|
||||||
# type_elements = c_elements.name
|
|
||||||
type_elements = c_elements
|
type_elements = c_elements
|
||||||
for inp_element in input_elements:
|
for inp_element in input_elements:
|
||||||
if type_elements == inp_element:
|
if type_elements == inp_element:
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ def do_calibre_export(book_id, book_format):
|
|||||||
my_env = os.environ.copy()
|
my_env = os.environ.copy()
|
||||||
if config.config_calibre_split:
|
if config.config_calibre_split:
|
||||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
library_path = config.config_calibre_split_dir
|
library_path = config.get_book_path()
|
||||||
else:
|
|
||||||
library_path = config.config_calibre_dir
|
|
||||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
||||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||||
str(book_id)]
|
str(book_id)]
|
||||||
|
|||||||
10
cps/epub.py
10
cps/epub.py
@@ -25,6 +25,7 @@ from . import config, logger
|
|||||||
from .helper import split_authors
|
from .helper import split_authors
|
||||||
from .epub_helper import get_content_opf, default_ns
|
from .epub_helper import get_content_opf, default_ns
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ def get_epub_layout(book, book_data):
|
|||||||
return layout[0]
|
return layout[0]
|
||||||
|
|
||||||
|
|
||||||
def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
def get_epub_info(tmp_file_path, original_file_name, original_file_extension, no_cover_processing):
|
||||||
ns = {
|
ns = {
|
||||||
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
||||||
'pkg': 'http://www.idpf.org/2007/opf',
|
'pkg': 'http://www.idpf.org/2007/opf',
|
||||||
@@ -90,7 +91,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
elif s == 'date':
|
elif s == 'date':
|
||||||
epub_metadata[s] = tmp[0][:10]
|
epub_metadata[s] = tmp[0][:10]
|
||||||
else:
|
else:
|
||||||
epub_metadata[s] = tmp[0].strip()
|
epub_metadata[s] = strip_whitespaces(tmp[0])
|
||||||
else:
|
else:
|
||||||
epub_metadata[s] = 'Unknown'
|
epub_metadata[s] = 'Unknown'
|
||||||
|
|
||||||
@@ -116,7 +117,10 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
epub_metadata = parse_epub_series(ns, tree, epub_metadata)
|
epub_metadata = parse_epub_series(ns, tree, epub_metadata)
|
||||||
|
|
||||||
epub_zip = zipfile.ZipFile(tmp_file_path)
|
epub_zip = zipfile.ZipFile(tmp_file_path)
|
||||||
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
|
if not no_cover_processing:
|
||||||
|
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
|
||||||
|
else:
|
||||||
|
cover_file = None
|
||||||
|
|
||||||
identifiers = []
|
identifiers = []
|
||||||
for node in p.xpath('dc:identifier', namespaces=ns):
|
for node in p.xpath('dc:identifier', namespaces=ns):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import re
|
|||||||
import regex
|
import regex
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
import requests
|
import requests
|
||||||
import unidecode
|
import unidecode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -52,6 +52,7 @@ except ImportError:
|
|||||||
UnacceptableAddressException = MissingSchema = BaseException
|
UnacceptableAddressException = MissingSchema = BaseException
|
||||||
|
|
||||||
from . import calibre_db, cli_param
|
from . import calibre_db, cli_param
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
from .tasks.convert import TaskConvert
|
from .tasks.convert import TaskConvert
|
||||||
from . import logger, config, db, ub, fs
|
from . import logger, config, db, ub, fs
|
||||||
from . import gdriveutils as gd
|
from . import gdriveutils as gd
|
||||||
@@ -118,7 +119,7 @@ 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):
|
||||||
for email in ereader_mail.split(','):
|
for email in ereader_mail.split(','):
|
||||||
email = email.strip()
|
email = strip_whitespaces(email)
|
||||||
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
||||||
config.get_mail_settings(), email, N_("Test Email"),
|
config.get_mail_settings(), email, N_("Test Email"),
|
||||||
_('This Email has been sent via Calibre-Web.')))
|
_('This Email has been sent via Calibre-Web.')))
|
||||||
@@ -228,7 +229,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id)
|
|||||||
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_("%(book)s send to eReader", book=link)
|
email_text = N_("%(book)s send to eReader", book=link)
|
||||||
for email in ereader_mail.split(','):
|
for email in ereader_mail.split(','):
|
||||||
email = email.strip()
|
email = strip_whitespaces(email)
|
||||||
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), email,
|
config.get_mail_settings(), email,
|
||||||
email_text, _('This Email has been sent via Calibre-Web.'), book.id))
|
email_text, _('This Email has been sent via Calibre-Web.'), book.id))
|
||||||
@@ -252,7 +253,7 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
|
|||||||
# pipe has to be replaced with comma
|
# pipe has to be replaced with comma
|
||||||
value = re.sub(r'[|]+', ',', 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 = strip_whitespaces(value.encode('utf-8')[:chars].decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
raise ValueError("Filename cannot be empty")
|
raise ValueError("Filename cannot be empty")
|
||||||
@@ -267,11 +268,11 @@ def split_authors(values):
|
|||||||
commas = author.count(',')
|
commas = author.count(',')
|
||||||
if commas == 1:
|
if commas == 1:
|
||||||
author_split = author.split(',')
|
author_split = author.split(',')
|
||||||
authors_list.append(author_split[1].strip() + ' ' + author_split[0].strip())
|
authors_list.append(strip_whitespaces(author_split[1]) + ' ' + strip_whitespaces(author_split[0]))
|
||||||
elif commas > 1:
|
elif commas > 1:
|
||||||
authors_list.extend([x.strip() for x in author.split(',')])
|
authors_list.extend([strip_whitespaces(x) for x in author.split(',')])
|
||||||
else:
|
else:
|
||||||
authors_list.append(author.strip())
|
authors_list.append(strip_whitespaces(author))
|
||||||
return authors_list
|
return authors_list
|
||||||
|
|
||||||
|
|
||||||
@@ -327,7 +328,7 @@ def edit_book_read_status(book_id, read_status=None):
|
|||||||
ub.session_commit("Book {} readbit toggled".format(book_id))
|
ub.session_commit("Book {} readbit toggled".format(book_id))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.create_functions(config)
|
||||||
book = calibre_db.get_filtered_book(book_id)
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
|
book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
|
||||||
if len(book_read_status):
|
if len(book_read_status):
|
||||||
@@ -661,7 +662,7 @@ def check_email(email):
|
|||||||
|
|
||||||
|
|
||||||
def check_username(username):
|
def check_username(username):
|
||||||
username = username.strip()
|
username = strip_whitespaces(username)
|
||||||
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("This username is already taken")
|
log.error("This username is already taken")
|
||||||
raise Exception(_("This username is already taken"))
|
raise Exception(_("This username is already taken"))
|
||||||
@@ -669,16 +670,18 @@ def check_username(username):
|
|||||||
|
|
||||||
|
|
||||||
def valid_email(emails):
|
def valid_email(emails):
|
||||||
|
valid_emails = []
|
||||||
for email in emails.split(','):
|
for email in emails.split(','):
|
||||||
email = email.strip()
|
email = strip_whitespaces(email)
|
||||||
# if email is not deleted
|
# if email is not deleted
|
||||||
if email:
|
if email:
|
||||||
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||||
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||||
email):
|
email):
|
||||||
log.error("Invalid Email address format")
|
log.error("Invalid Email address format for {}".format(email))
|
||||||
raise Exception(_("Invalid Email address format"))
|
raise Exception(_("Invalid Email address format"))
|
||||||
return email
|
valid_emails.append(email)
|
||||||
|
return ",".join(valid_emails)
|
||||||
|
|
||||||
|
|
||||||
def valid_password(check_password):
|
def valid_password(check_password):
|
||||||
@@ -788,24 +791,23 @@ def get_book_cover_internal(book, resolution=None):
|
|||||||
|
|
||||||
def get_book_cover_thumbnail(book, resolution):
|
def get_book_cover_thumbnail(book, resolution):
|
||||||
if book and book.has_cover:
|
if book and book.has_cover:
|
||||||
return ub.session \
|
return (ub.session
|
||||||
.query(ub.Thumbnail) \
|
.query(ub.Thumbnail)
|
||||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER) \
|
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER)
|
||||||
.filter(ub.Thumbnail.entity_id == book.id) \
|
.filter(ub.Thumbnail.entity_id == book.id)
|
||||||
.filter(ub.Thumbnail.resolution == resolution) \
|
.filter(ub.Thumbnail.resolution == resolution)
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||||
.first()
|
.first())
|
||||||
|
|
||||||
|
|
||||||
def get_series_thumbnail_on_failure(series_id, resolution):
|
def get_series_thumbnail_on_failure(series_id, resolution):
|
||||||
book = calibre_db.session \
|
book = (calibre_db.session
|
||||||
.query(db.Books) \
|
.query(db.Books)
|
||||||
.join(db.books_series_link) \
|
.join(db.books_series_link)
|
||||||
.join(db.Series) \
|
.join(db.Series)
|
||||||
.filter(db.Series.id == series_id) \
|
.filter(db.Series.id == series_id)
|
||||||
.filter(db.Books.has_cover == 1) \
|
.filter(db.Books.has_cover == 1)
|
||||||
.first()
|
.first())
|
||||||
|
|
||||||
return get_book_cover_internal(book, resolution=resolution)
|
return get_book_cover_internal(book, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
@@ -827,13 +829,13 @@ def get_series_cover_internal(series_id, resolution=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_series_thumbnail(series_id, resolution):
|
def get_series_thumbnail(series_id, resolution):
|
||||||
return ub.session \
|
return (ub.session
|
||||||
.query(ub.Thumbnail) \
|
.query(ub.Thumbnail)
|
||||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES) \
|
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES)
|
||||||
.filter(ub.Thumbnail.entity_id == series_id) \
|
.filter(ub.Thumbnail.entity_id == series_id)
|
||||||
.filter(ub.Thumbnail.resolution == resolution) \
|
.filter(ub.Thumbnail.resolution == resolution)
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||||
.first()
|
.first())
|
||||||
|
|
||||||
|
|
||||||
# saves book cover from url
|
# saves book cover from url
|
||||||
|
|||||||
@@ -15,24 +15,30 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import sys
|
||||||
|
|
||||||
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
||||||
from . import logger
|
from . import logger
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from iso639 import languages, __version__
|
from iso639 import languages
|
||||||
|
# iso_version = importlib.metadata.version("iso639")
|
||||||
get = languages.get
|
get = languages.get
|
||||||
except ImportError:
|
|
||||||
from pycountry import languages as pyc_languages
|
|
||||||
try:
|
try:
|
||||||
import pkg_resources
|
if sys.version_info >= (3, 12):
|
||||||
__version__ = pkg_resources.get_distribution('pycountry').version + ' (PyCountry)'
|
import pkg_resources
|
||||||
del pkg_resources
|
except ImportError:
|
||||||
except (ImportError, Exception):
|
print("Python 3.12 isn't compatible with iso-639. Please install pycountry.")
|
||||||
__version__ = "? (PyCountry)"
|
except ImportError as ex:
|
||||||
|
from pycountry import languages as pyc_languages
|
||||||
|
#try:
|
||||||
|
# iso_version = importlib.metadata.version("pycountry") + ' (PyCountry)'
|
||||||
|
#except (ImportError, Exception):
|
||||||
|
# iso_version = "?" + ' (PyCountry)'
|
||||||
|
|
||||||
def _copy_fields(l):
|
def _copy_fields(l):
|
||||||
l.part1 = getattr(l, 'alpha_2', None)
|
l.part1 = getattr(l, 'alpha_2', None)
|
||||||
@@ -69,20 +75,20 @@ def get_language_name(locale, lang_code):
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def get_language_codes(locale, language_names, remainder=None):
|
def get_language_code_from_name(locale, language_names, remainder=None):
|
||||||
language_names = set(x.strip().lower() for x in language_names if x)
|
language_names = set(strip_whitespaces(x).lower() for x in language_names if x)
|
||||||
lang = list()
|
lang = list()
|
||||||
for k, v in get_language_names(locale).items():
|
for key, val in get_language_names(locale).items():
|
||||||
v = v.lower()
|
val = val.lower()
|
||||||
if v in language_names:
|
if val in language_names:
|
||||||
lang.append(k)
|
lang.append(key)
|
||||||
language_names.remove(v)
|
language_names.remove(val)
|
||||||
if remainder is not None and language_names:
|
if remainder is not None and language_names:
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
return lang
|
return lang
|
||||||
|
|
||||||
|
|
||||||
def get_valid_language_codes(locale, language_names, remainder=None):
|
def get_valid_language_codes_from_code(locale, language_names, remainder=None):
|
||||||
lang = list()
|
lang = list()
|
||||||
if "" in language_names:
|
if "" in language_names:
|
||||||
language_names.remove("")
|
language_names.remove("")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import datetime
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import Blueprint, request, url_for
|
from flask import Blueprint, request, url_for, g
|
||||||
from flask_babel import format_date
|
from flask_babel import format_date
|
||||||
from .cw_login import current_user
|
from .cw_login import current_user
|
||||||
|
|
||||||
@@ -112,7 +112,10 @@ def yesno(value, yes, no):
|
|||||||
@jinjia.app_template_filter('formatfloat')
|
@jinjia.app_template_filter('formatfloat')
|
||||||
def formatfloat(value, decimals=1):
|
def formatfloat(value, decimals=1):
|
||||||
value = 0 if not value else value
|
value = 0 if not value else value
|
||||||
return ('{0:.' + str(decimals) + 'f}').format(value).rstrip('0').rstrip('.')
|
formated_value = ('{0:.' + str(decimals) + 'f}').format(value)
|
||||||
|
if formated_value.endswith('.' + "0" * decimals):
|
||||||
|
formated_value = formated_value.rstrip('0').rstrip('.')
|
||||||
|
return formated_value
|
||||||
|
|
||||||
|
|
||||||
@jinjia.app_template_filter('formatseriesindex')
|
@jinjia.app_template_filter('formatseriesindex')
|
||||||
@@ -179,3 +182,12 @@ def get_cover_srcset(series):
|
|||||||
url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp())
|
url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp())
|
||||||
srcset.append(f'{url} {resolution}x')
|
srcset.append(f'{url} {resolution}x')
|
||||||
return ', '.join(srcset)
|
return ', '.join(srcset)
|
||||||
|
|
||||||
|
|
||||||
|
@jinjia.app_template_filter('music')
|
||||||
|
def contains_music(book_formats):
|
||||||
|
result = False
|
||||||
|
for format in book_formats:
|
||||||
|
if format.format.lower() in g.constants.EXTENSIONS_AUDIO:
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|||||||
25
cps/kobo.py
25
cps/kobo.py
@@ -18,7 +18,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
from datetime import datetime, timezone
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -47,7 +47,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 . import isoLanguages
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
from .constants import COVER_THUMBNAIL_SMALL
|
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
from .web import download_required
|
from .web import download_required
|
||||||
@@ -131,7 +131,7 @@ def convert_to_kobo_timestamp_string(timestamp):
|
|||||||
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
log.debug("Timestamp not valid: {}".format(exc))
|
log.debug("Timestamp not valid: {}".format(exc))
|
||||||
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
@kobo.route("/v1/library/sync")
|
@kobo.route("/v1/library/sync")
|
||||||
@@ -150,15 +150,15 @@ def HandleSyncRequest():
|
|||||||
|
|
||||||
# if no books synced don't respect sync_token
|
# if no books synced don't respect sync_token
|
||||||
if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count():
|
if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count():
|
||||||
sync_token.books_last_modified = datetime.datetime.min
|
sync_token.books_last_modified = datetime.min
|
||||||
sync_token.books_last_created = datetime.datetime.min
|
sync_token.books_last_created = datetime.min
|
||||||
sync_token.reading_state_last_modified = datetime.datetime.min
|
sync_token.reading_state_last_modified = datetime.min
|
||||||
|
|
||||||
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
||||||
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
||||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||||
|
|
||||||
new_archived_last_modified = datetime.datetime.min
|
new_archived_last_modified = datetime.min
|
||||||
sync_results = []
|
sync_results = []
|
||||||
|
|
||||||
# We reload the book database so that the user gets a fresh view of the library
|
# We reload the book database so that the user gets a fresh view of the library
|
||||||
@@ -375,7 +375,7 @@ def create_book_entitlement(book, archived):
|
|||||||
book_uuid = str(book.uuid)
|
book_uuid = str(book.uuid)
|
||||||
return {
|
return {
|
||||||
"Accessibility": "Full",
|
"Accessibility": "Full",
|
||||||
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.utcnow())},
|
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.now(timezone.utc))},
|
||||||
"Created": convert_to_kobo_timestamp_string(book.timestamp),
|
"Created": convert_to_kobo_timestamp_string(book.timestamp),
|
||||||
"CrossRevisionId": book_uuid,
|
"CrossRevisionId": book_uuid,
|
||||||
"Id": book_uuid,
|
"Id": book_uuid,
|
||||||
@@ -795,7 +795,7 @@ def HandleStateRequest(book_uuid):
|
|||||||
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
|
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
|
||||||
and new_book_read_status != book_read.read_status:
|
and new_book_read_status != book_read.read_status:
|
||||||
book_read.times_started_reading += 1
|
book_read.times_started_reading += 1
|
||||||
book_read.last_time_started_reading = datetime.datetime.utcnow()
|
book_read.last_time_started_reading = datetime.now(timezone.utc)
|
||||||
book_read.read_status = new_book_read_status
|
book_read.read_status = new_book_read_status
|
||||||
update_results_response["StatusInfoResult"] = {"Result": "Success"}
|
update_results_response["StatusInfoResult"] = {"Result": "Success"}
|
||||||
except (KeyError, TypeError, ValueError, StatementError):
|
except (KeyError, TypeError, ValueError, StatementError):
|
||||||
@@ -903,7 +903,12 @@ def get_current_bookmark_response(current_bookmark):
|
|||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
||||||
try:
|
try:
|
||||||
resolution = None if int(height) > 1000 else COVER_THUMBNAIL_SMALL
|
if int(height) > 1000:
|
||||||
|
resolution = COVER_THUMBNAIL_LARGE
|
||||||
|
elif int(height) > 500:
|
||||||
|
resolution = COVER_THUMBNAIL_MEDIUM
|
||||||
|
else:
|
||||||
|
resolution = COVER_THUMBNAIL_SMALL
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.error("Requested height %s of book %s is invalid" % (book_uuid, height))
|
log.error("Requested height %s of book %s is invalid" % (book_uuid, height))
|
||||||
resolution = COVER_THUMBNAIL_SMALL
|
resolution = COVER_THUMBNAIL_SMALL
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
from .cw_login import current_user
|
from .cw_login import current_user
|
||||||
from . import ub
|
from . import ub
|
||||||
import datetime
|
from datetime import datetime, timezone
|
||||||
from sqlalchemy.sql.expression import or_, and_, true
|
from sqlalchemy.sql.expression import or_, and_, true
|
||||||
# from sqlalchemy import exc
|
# from sqlalchemy import exc
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ def change_archived_books(book_id, state=None, message=None):
|
|||||||
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||||
|
|
||||||
archived_book.is_archived = state if state != None else not archived_book.is_archived
|
archived_book.is_archived = state if state != None else not archived_book.is_archived
|
||||||
archived_book.last_modified = datetime.datetime.utcnow() # toDo. Check utc timestamp
|
archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
|
||||||
|
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
ub.session_commit(message)
|
ub.session_commit(message)
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class Amazon(Metadata):
|
|||||||
def search(
|
def search(
|
||||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||||
) -> Optional[List[MetaRecord]]:
|
) -> Optional[List[MetaRecord]]:
|
||||||
#timer=time()
|
|
||||||
def inner(link, index) -> [dict, int]:
|
def inner(link, index) -> [dict, int]:
|
||||||
with self.session as session:
|
with self.session as session:
|
||||||
try:
|
try:
|
||||||
@@ -61,11 +60,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 None
|
return []
|
||||||
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 None
|
return []
|
||||||
try:
|
try:
|
||||||
match = MetaRecord(
|
match = MetaRecord(
|
||||||
title = "",
|
title = "",
|
||||||
@@ -88,7 +87,7 @@ class Amazon(Metadata):
|
|||||||
soup2.find("div", attrs={"data-feature-name": "bookDescription"}).stripped_strings)\
|
soup2.find("div", attrs={"data-feature-name": "bookDescription"}).stripped_strings)\
|
||||||
.replace("\xa0"," ")[:-9].strip().strip("\n")
|
.replace("\xa0"," ")[:-9].strip().strip("\n")
|
||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
return None # if there is no description it is not a book and therefore should be ignored
|
return [] # if there is no description it is not a book and therefore should be ignored
|
||||||
try:
|
try:
|
||||||
match.title = soup2.find("span", attrs={"id": "productTitle"}).text
|
match.title = soup2.find("span", attrs={"id": "productTitle"}).text
|
||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
@@ -113,7 +112,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 None
|
return []
|
||||||
|
|
||||||
val = list()
|
val = list()
|
||||||
if self.active:
|
if self.active:
|
||||||
@@ -134,6 +133,6 @@ class Amazon(Metadata):
|
|||||||
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:5])}
|
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:5])}
|
||||||
val = list(map(lambda x : x.result() ,concurrent.futures.as_completed(fut)))
|
val = list(map(lambda x : x.result(), concurrent.futures.as_completed(fut)))
|
||||||
result = list(filter(lambda x: x, val))
|
result = list(filter(lambda x: x, val))
|
||||||
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
|
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Google(Metadata):
|
|||||||
results.raise_for_status()
|
results.raise_for_status()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
for result in results.json().get("items", []):
|
for result in results.json().get("items", []):
|
||||||
val.append(
|
val.append(
|
||||||
self._parse_search_result(
|
self._parse_search_result(
|
||||||
|
|||||||
0
cps/redirect.py
Executable file → Normal file
0
cps/redirect.py
Executable file → Normal file
@@ -26,6 +26,7 @@ from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
|
|||||||
from sqlalchemy.sql.functions import coalesce
|
from sqlalchemy.sql.functions import coalesce
|
||||||
|
|
||||||
from . import logger, db, calibre_db, config, ub
|
from . import logger, db, calibre_db, config, ub
|
||||||
|
from .string_helper import strip_whitespaces
|
||||||
from .usermanagement import login_required_if_no_ano
|
from .usermanagement import login_required_if_no_ano
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
@@ -81,16 +82,27 @@ def adv_search_custom_columns(cc, term, q):
|
|||||||
if custom_end:
|
if custom_end:
|
||||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||||
func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
|
func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
|
||||||
|
elif c.datatype in ["int", "float"]:
|
||||||
|
custom_low = term.get('custom_column_' + str(c.id) + '_low')
|
||||||
|
custom_high = term.get('custom_column_' + str(c.id) + '_high')
|
||||||
|
if custom_low:
|
||||||
|
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||||
|
db.cc_classes[c.id].value >= custom_low))
|
||||||
|
if custom_high:
|
||||||
|
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||||
|
db.cc_classes[c.id].value <= custom_high))
|
||||||
else:
|
else:
|
||||||
custom_query = term.get('custom_column_' + str(c.id))
|
custom_query = term.get('custom_column_' + str(c.id))
|
||||||
if custom_query != '' and custom_query is not None:
|
if c.datatype == 'bool':
|
||||||
if c.datatype == 'bool':
|
if custom_query != "Any":
|
||||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
if custom_query == "":
|
||||||
db.cc_classes[c.id].value == (custom_query == "True")))
|
q = q.filter(~getattr(db.Books, 'custom_column_' + str(c.id)).
|
||||||
elif c.datatype == 'int' or c.datatype == 'float':
|
any(db.cc_classes[c.id].value >= 0))
|
||||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
else:
|
||||||
db.cc_classes[c.id].value == custom_query))
|
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||||
elif c.datatype == 'rating':
|
db.cc_classes[c.id].value == bool(custom_query == "True")))
|
||||||
|
elif custom_query != '' and custom_query is not None:
|
||||||
|
if c.datatype == 'rating':
|
||||||
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
|
||||||
db.cc_classes[c.id].value == int(float(custom_query) * 2)))
|
db.cc_classes[c.id].value == int(float(custom_query) * 2)))
|
||||||
else:
|
else:
|
||||||
@@ -129,10 +141,10 @@ def adv_search_read_status(read_status):
|
|||||||
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
|
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if read_status == "True":
|
if read_status == "":
|
||||||
db_filter = db.cc_classes[config.config_read_column].value == True
|
db_filter = coalesce(db.cc_classes[config.config_read_column].value, 2) == 2
|
||||||
else:
|
else:
|
||||||
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
|
db_filter = db.cc_classes[config.config_read_column].value == bool(read_status == "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.%(column)d does not exist in calibre database",
|
flash(_("Custom Column No.%(column)d does not exist in calibre database",
|
||||||
@@ -232,7 +244,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
pagination = None
|
pagination = None
|
||||||
|
|
||||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||||
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
calibre_db.create_functions()
|
||||||
|
# calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
||||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||||
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
||||||
.outerjoin(db.Series)\
|
.outerjoin(db.Series)\
|
||||||
@@ -245,8 +258,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
tags['include_' + element] = term.get('include_' + element)
|
tags['include_' + element] = term.get('include_' + element)
|
||||||
tags['exclude_' + element] = term.get('exclude_' + element)
|
tags['exclude_' + element] = term.get('exclude_' + element)
|
||||||
|
|
||||||
author_name = term.get("author_name")
|
author_name = term.get("authors")
|
||||||
book_title = term.get("book_title")
|
book_title = term.get("title")
|
||||||
publisher = term.get("publisher")
|
publisher = term.get("publisher")
|
||||||
pub_start = term.get("publishstart")
|
pub_start = term.get("publishstart")
|
||||||
pub_end = term.get("publishend")
|
pub_end = term.get("publishend")
|
||||||
@@ -255,11 +268,11 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
description = term.get("comment")
|
description = term.get("comment")
|
||||||
read_status = term.get("read_status")
|
read_status = term.get("read_status")
|
||||||
if author_name:
|
if author_name:
|
||||||
author_name = author_name.strip().lower().replace(',', '|')
|
author_name = strip_whitespaces(author_name).lower().replace(',', '|')
|
||||||
if book_title:
|
if book_title:
|
||||||
book_title = book_title.strip().lower()
|
book_title = strip_whitespaces(book_title).lower()
|
||||||
if publisher:
|
if publisher:
|
||||||
publisher = publisher.strip().lower()
|
publisher = strip_whitespaces(publisher).lower()
|
||||||
|
|
||||||
search_term = []
|
search_term = []
|
||||||
cc_present = False
|
cc_present = False
|
||||||
@@ -275,10 +288,23 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
cc_present = True
|
cc_present = True
|
||||||
if column_end:
|
if column_end:
|
||||||
search_term.extend(["{} <= {}".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
|
||||||
|
if c.datatype in ["int", "float"]:
|
||||||
|
column_low = term.get('custom_column_' + str(c.id) + '_low')
|
||||||
|
column_high = term.get('custom_column_' + str(c.id) + '_high')
|
||||||
|
if column_low:
|
||||||
|
search_term.extend(["{} >= {}".format(c.name, column_low)])
|
||||||
|
cc_present = True
|
||||||
|
if column_high:
|
||||||
|
search_term.extend(["{} <= {}".format(c.name,column_high)])
|
||||||
|
cc_present = True
|
||||||
|
elif c.datatype == "bool":
|
||||||
|
if term.get('custom_column_' + str(c.id)) != "Any":
|
||||||
|
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||||
|
cc_present = True
|
||||||
elif term.get('custom_column_' + str(c.id)):
|
elif term.get('custom_column_' + str(c.id)):
|
||||||
search_term.extend([("{}: {}".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
|
||||||
|
|||||||
66
cps/shelf.py
66
cps/shelf.py
@@ -21,7 +21,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from flask import Blueprint, flash, redirect, request, url_for, abort
|
from flask import Blueprint, flash, redirect, request, url_for, abort
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@@ -80,7 +80,7 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
return "%s is a invalid Book Id. Could not be added to Shelf" % book_id, 400
|
return "%s is a invalid Book Id. Could not be added to Shelf" % book_id, 400
|
||||||
|
|
||||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
|
||||||
shelf.last_modified = datetime.utcnow()
|
shelf.last_modified = datetime.now(timezone.utc)
|
||||||
try:
|
try:
|
||||||
ub.session.merge(shelf)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@@ -139,7 +139,7 @@ def search_to_shelf(shelf_id):
|
|||||||
for book in books_for_shelf:
|
for book in books_for_shelf:
|
||||||
maxOrder += 1
|
maxOrder += 1
|
||||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||||
shelf.last_modified = datetime.utcnow()
|
shelf.last_modified = datetime.now(timezone.utc)
|
||||||
try:
|
try:
|
||||||
ub.session.merge(shelf)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@@ -185,7 +185,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ub.session.delete(book_shelf)
|
ub.session.delete(book_shelf)
|
||||||
shelf.last_modified = datetime.utcnow()
|
shelf.last_modified = datetime.now(timezone.utc)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
@@ -250,7 +250,7 @@ def show_simpleshelf(shelf_id):
|
|||||||
return render_show_shelf(2, shelf_id, 1, None)
|
return render_show_shelf(2, shelf_id, 1, None)
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
|
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "stored", 'page': 1})
|
||||||
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
|
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
|
||||||
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
|
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@@ -271,7 +271,7 @@ 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 different from before -> shelf.last_modified = datetime.utcnow()
|
# if order different from before -> shelf.last_modified = datetime.now(timezone.utc)
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
@@ -418,29 +418,37 @@ def change_shelf_order(shelf_id, order):
|
|||||||
|
|
||||||
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||||
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()
|
||||||
|
status = current_user.get_view_property("shelf", 'man')
|
||||||
# check user is allowed to access shelf
|
# check user is allowed to access shelf
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
if shelf_type == 1:
|
if shelf_type == 1:
|
||||||
# order = [ub.BookShelf.order.asc()]
|
if status != 'on':
|
||||||
if sort_param == 'pubnew':
|
if sort_param == 'stored':
|
||||||
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
|
sort_param = current_user.get_view_property("shelf", 'stored')
|
||||||
if sort_param == 'pubold':
|
else:
|
||||||
change_shelf_order(shelf_id, [db.Books.pubdate])
|
current_user.set_view_property("shelf", 'stored', sort_param)
|
||||||
if sort_param == 'abc':
|
if sort_param == 'pubnew':
|
||||||
change_shelf_order(shelf_id, [db.Books.sort])
|
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
|
||||||
if sort_param == 'zyx':
|
if sort_param == 'pubold':
|
||||||
change_shelf_order(shelf_id, [db.Books.sort.desc()])
|
change_shelf_order(shelf_id, [db.Books.pubdate])
|
||||||
if sort_param == 'new':
|
if sort_param == 'shelfnew':
|
||||||
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
|
change_shelf_order(shelf_id, [ub.BookShelf.date_added.desc()])
|
||||||
if sort_param == 'old':
|
if sort_param == 'shelfold':
|
||||||
change_shelf_order(shelf_id, [db.Books.timestamp])
|
change_shelf_order(shelf_id, [ub.BookShelf.date_added])
|
||||||
if sort_param == 'authaz':
|
if sort_param == 'abc':
|
||||||
change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
|
change_shelf_order(shelf_id, [db.Books.sort])
|
||||||
if sort_param == 'authza':
|
if sort_param == 'zyx':
|
||||||
change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
|
change_shelf_order(shelf_id, [db.Books.sort.desc()])
|
||||||
db.Series.name.desc(),
|
if sort_param == 'new':
|
||||||
db.Books.series_index.desc()])
|
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
|
||||||
|
if sort_param == 'old':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.timestamp])
|
||||||
|
if sort_param == 'authaz':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
|
||||||
|
if sort_param == 'authza':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
|
||||||
|
db.Series.name.desc(),
|
||||||
|
db.Books.series_index.desc()])
|
||||||
page = "shelf.html"
|
page = "shelf.html"
|
||||||
pagesize = 0
|
pagesize = 0
|
||||||
else:
|
else:
|
||||||
@@ -453,7 +461,7 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
[ub.BookShelf.order.asc()],
|
[ub.BookShelf.order.asc()],
|
||||||
True, config.config_read_column,
|
True, config.config_read_column,
|
||||||
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
|
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
|
||||||
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
|
# delete shelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
|
||||||
wrong_entries = calibre_db.session.query(ub.BookShelf) \
|
wrong_entries = calibre_db.session.query(ub.BookShelf) \
|
||||||
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
|
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
|
||||||
.filter(db.Books.id == None).all()
|
.filter(db.Books.id == None).all()
|
||||||
@@ -472,7 +480,9 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
title=_("Shelf: '%(name)s'", name=shelf.name),
|
title=_("Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf,
|
shelf=shelf,
|
||||||
page="shelf")
|
page="shelf",
|
||||||
|
status=status,
|
||||||
|
order=sort_param)
|
||||||
else:
|
else:
|
||||||
flash(_("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"))
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
/* global Bloodhound, language, Modernizr, tinymce, getPath */
|
/* global Bloodhound, language, Modernizr, tinymce, getPath */
|
||||||
|
|
||||||
if ($("#description").length) {
|
if ($("#comments").length) {
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: "#description",
|
selector: "#comments",
|
||||||
plugins: 'code',
|
plugins: 'code',
|
||||||
branding: false,
|
branding: false,
|
||||||
menubar: "edit view format",
|
menubar: "edit view format",
|
||||||
@@ -93,7 +93,7 @@ var authors = new Bloodhound({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".form-group #bookAuthor").typeahead(
|
$(".form-group #authors").typeahead(
|
||||||
{
|
{
|
||||||
highlight: true,
|
highlight: true,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
@@ -243,13 +243,13 @@ $("#search").on("change input.typeahead:selected", function(event) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn-upload-format").on("change", function () {
|
/*$("#btn-upload-format").on("change", function () {
|
||||||
var filename = $(this).val();
|
var filename = $(this).val();
|
||||||
if (filename.substring(3, 11) === "fakepath") {
|
if (filename.substring(3, 11) === "fakepath") {
|
||||||
filename = filename.substring(12);
|
filename = filename.substring(12);
|
||||||
} // Remove c:\fake at beginning from localhost chrome
|
} // Remove c:\fake at beginning from localhost chrome
|
||||||
$("#upload-format").text(filename);
|
$("#upload-format").text(filename);
|
||||||
});
|
});*/
|
||||||
|
|
||||||
$("#btn-upload-cover").on("change", function () {
|
$("#btn-upload-cover").on("change", function () {
|
||||||
var filename = $(this).val();
|
var filename = $(this).val();
|
||||||
@@ -261,8 +261,8 @@ $("#btn-upload-cover").on("change", function () {
|
|||||||
|
|
||||||
$("#xchange").click(function () {
|
$("#xchange").click(function () {
|
||||||
this.blur();
|
this.blur();
|
||||||
var title = $("#book_title").val();
|
var title = $("#title").val();
|
||||||
$("#book_title").val($("#bookAuthor").val());
|
$("#title").val($("#authors").val());
|
||||||
$("#bookAuthor").val(title);
|
$("#authors").val(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ $(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populateForm (book) {
|
function populateForm (book) {
|
||||||
tinymce.get("description").setContent(book.description);
|
tinymce.get("comments").setContent(book.description);
|
||||||
var uniqueTags = getUniqueValues('tags', book)
|
var uniqueTags = getUniqueValues('tags', book)
|
||||||
var uniqueLanguages = getUniqueValues('languages', book)
|
var uniqueLanguages = getUniqueValues('languages', book)
|
||||||
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
||||||
$("#bookAuthor").val(ampSeparatedAuthors);
|
$("#authors").val(ampSeparatedAuthors);
|
||||||
$("#book_title").val(book.title);
|
$("#title").val(book.title);
|
||||||
$("#tags").val(uniqueTags.join(", "));
|
$("#tags").val(uniqueTags.join(", "));
|
||||||
$("#languages").val(uniqueLanguages.join(", "));
|
$("#languages").val(uniqueLanguages.join(", "));
|
||||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||||
@@ -172,7 +172,7 @@ $(function () {
|
|||||||
|
|
||||||
$("#get_meta").click(function () {
|
$("#get_meta").click(function () {
|
||||||
populate_provider();
|
populate_provider();
|
||||||
var bookTitle = $("#book_title").val();
|
var bookTitle = $("#title").val();
|
||||||
$("#keyword").val(bookTitle);
|
$("#keyword").val(bookTitle);
|
||||||
keyword = bookTitle;
|
keyword = bookTitle;
|
||||||
doSearch(bookTitle);
|
doSearch(bookTitle);
|
||||||
|
|||||||
5318
cps/static/js/libs/pdf.mjs
vendored
5318
cps/static/js/libs/pdf.mjs
vendored
File diff suppressed because it is too large
Load Diff
5316
cps/static/js/libs/pdf.worker.mjs
vendored
5316
cps/static/js/libs/pdf.worker.mjs
vendored
File diff suppressed because it is too large
Load Diff
456
cps/static/js/libs/viewer.mjs
vendored
456
cps/static/js/libs/viewer.mjs
vendored
@@ -2,7 +2,7 @@
|
|||||||
* @licstart The following is the entire license notice for the
|
* @licstart The following is the entire license notice for the
|
||||||
* JavaScript code in this page
|
* JavaScript code in this page
|
||||||
*
|
*
|
||||||
* Copyright 2023 Mozilla Foundation
|
* Copyright 2024 Mozilla Foundation
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -446,7 +446,7 @@ class ProgressBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDisableAutoFetch(delay = 5000) {
|
setDisableAutoFetch(delay = 5000) {
|
||||||
if (isNaN(this.#percent)) {
|
if (this.#percent === 100 || isNaN(this.#percent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.#disableAutoFetchTimeout) {
|
if (this.#disableAutoFetchTimeout) {
|
||||||
@@ -535,15 +535,20 @@ function toggleExpandedBtn(button, toggle, view = null) {
|
|||||||
|
|
||||||
;// CONCATENATED MODULE: ./web/app_options.js
|
;// CONCATENATED MODULE: ./web/app_options.js
|
||||||
{
|
{
|
||||||
var compatibilityParams = Object.create(null);
|
var compatParams = new Map();
|
||||||
const userAgent = navigator.userAgent || "";
|
const userAgent = navigator.userAgent || "";
|
||||||
const platform = navigator.platform || "";
|
const platform = navigator.platform || "";
|
||||||
const maxTouchPoints = navigator.maxTouchPoints || 1;
|
const maxTouchPoints = navigator.maxTouchPoints || 1;
|
||||||
const isAndroid = /Android/.test(userAgent);
|
const isAndroid = /Android/.test(userAgent);
|
||||||
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
|
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
|
||||||
(function checkCanvasSizeLimitation() {
|
(function () {
|
||||||
if (isIOS || isAndroid) {
|
if (isIOS || isAndroid) {
|
||||||
compatibilityParams.maxCanvasPixels = 5242880;
|
compatParams.set("maxCanvasPixels", 5242880);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
(function () {
|
||||||
|
if (isAndroid) {
|
||||||
|
compatParams.set("useSystemFonts", false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -552,9 +557,21 @@ const OptionKind = {
|
|||||||
VIEWER: 0x02,
|
VIEWER: 0x02,
|
||||||
API: 0x04,
|
API: 0x04,
|
||||||
WORKER: 0x08,
|
WORKER: 0x08,
|
||||||
|
EVENT_DISPATCH: 0x10,
|
||||||
PREFERENCE: 0x80
|
PREFERENCE: 0x80
|
||||||
};
|
};
|
||||||
|
const Type = {
|
||||||
|
BOOLEAN: 0x01,
|
||||||
|
NUMBER: 0x02,
|
||||||
|
OBJECT: 0x04,
|
||||||
|
STRING: 0x08,
|
||||||
|
UNDEFINED: 0x10
|
||||||
|
};
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
|
allowedGlobalEvents: {
|
||||||
|
value: null,
|
||||||
|
kind: OptionKind.BROWSER
|
||||||
|
},
|
||||||
canvasMaxAreaInBytes: {
|
canvasMaxAreaInBytes: {
|
||||||
value: -1,
|
value: -1,
|
||||||
kind: OptionKind.BROWSER + OptionKind.API
|
kind: OptionKind.BROWSER + OptionKind.API
|
||||||
@@ -563,6 +580,16 @@ const defaultOptions = {
|
|||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.BROWSER
|
kind: OptionKind.BROWSER
|
||||||
},
|
},
|
||||||
|
localeProperties: {
|
||||||
|
value: {
|
||||||
|
lang: navigator.language || "en-US"
|
||||||
|
},
|
||||||
|
kind: OptionKind.BROWSER
|
||||||
|
},
|
||||||
|
nimbusDataStr: {
|
||||||
|
value: "",
|
||||||
|
kind: OptionKind.BROWSER
|
||||||
|
},
|
||||||
supportsCaretBrowsingMode: {
|
supportsCaretBrowsingMode: {
|
||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.BROWSER
|
kind: OptionKind.BROWSER
|
||||||
@@ -587,6 +614,14 @@ const defaultOptions = {
|
|||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.BROWSER
|
kind: OptionKind.BROWSER
|
||||||
},
|
},
|
||||||
|
toolbarDensity: {
|
||||||
|
value: 0,
|
||||||
|
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH
|
||||||
|
},
|
||||||
|
altTextLearnMoreUrl: {
|
||||||
|
value: "",
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
|
},
|
||||||
annotationEditorMode: {
|
annotationEditorMode: {
|
||||||
value: 0,
|
value: 0,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
@@ -619,6 +654,14 @@ const defaultOptions = {
|
|||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
},
|
},
|
||||||
|
enableAltText: {
|
||||||
|
value: false,
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
|
},
|
||||||
|
enableGuessAltText: {
|
||||||
|
value: true,
|
||||||
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
|
},
|
||||||
enableHighlightEditor: {
|
enableHighlightEditor: {
|
||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
@@ -627,10 +670,6 @@ const defaultOptions = {
|
|||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
},
|
},
|
||||||
enableML: {
|
|
||||||
value: false,
|
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
|
||||||
},
|
|
||||||
enablePermissions: {
|
enablePermissions: {
|
||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
@@ -643,8 +682,8 @@ const defaultOptions = {
|
|||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
},
|
},
|
||||||
enableStampEditor: {
|
enableUpdatedAddImage: {
|
||||||
value: true,
|
value: false,
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||||
},
|
},
|
||||||
externalLinkRel: {
|
externalLinkRel: {
|
||||||
@@ -775,6 +814,11 @@ const defaultOptions = {
|
|||||||
value: "../web/standard_fonts/",
|
value: "../web/standard_fonts/",
|
||||||
kind: OptionKind.API
|
kind: OptionKind.API
|
||||||
},
|
},
|
||||||
|
useSystemFonts: {
|
||||||
|
value: undefined,
|
||||||
|
kind: OptionKind.API,
|
||||||
|
type: Type.BOOLEAN + Type.UNDEFINED
|
||||||
|
},
|
||||||
verbosity: {
|
verbosity: {
|
||||||
value: 1,
|
value: 1,
|
||||||
kind: OptionKind.API
|
kind: OptionKind.API
|
||||||
@@ -807,62 +851,80 @@ const defaultOptions = {
|
|||||||
value: false,
|
value: false,
|
||||||
kind: OptionKind.VIEWER
|
kind: OptionKind.VIEWER
|
||||||
};
|
};
|
||||||
defaultOptions.locale = {
|
|
||||||
value: navigator.language || "en-US",
|
|
||||||
kind: OptionKind.VIEWER
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const userOptions = Object.create(null);
|
const userOptions = new Map();
|
||||||
{
|
{
|
||||||
for (const name in compatibilityParams) {
|
for (const [name, value] of compatParams) {
|
||||||
userOptions[name] = compatibilityParams[name];
|
userOptions.set(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AppOptions {
|
class AppOptions {
|
||||||
|
static eventBus;
|
||||||
constructor() {
|
constructor() {
|
||||||
throw new Error("Cannot initialize AppOptions.");
|
throw new Error("Cannot initialize AppOptions.");
|
||||||
}
|
}
|
||||||
static get(name) {
|
static get(name) {
|
||||||
return userOptions[name] ?? defaultOptions[name]?.value ?? undefined;
|
return userOptions.has(name) ? userOptions.get(name) : defaultOptions[name]?.value;
|
||||||
}
|
}
|
||||||
static getAll(kind = null, defaultOnly = false) {
|
static getAll(kind = null, defaultOnly = false) {
|
||||||
const options = Object.create(null);
|
const options = Object.create(null);
|
||||||
for (const name in defaultOptions) {
|
for (const name in defaultOptions) {
|
||||||
const defaultOption = defaultOptions[name];
|
const defaultOpt = defaultOptions[name];
|
||||||
if (kind && !(kind & defaultOption.kind)) {
|
if (kind && !(kind & defaultOpt.kind)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? defaultOption.value;
|
options[name] = !defaultOnly && userOptions.has(name) ? userOptions.get(name) : defaultOpt.value;
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
static set(name, value) {
|
static set(name, value) {
|
||||||
userOptions[name] = value;
|
this.setAll({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
static setAll(options, init = false) {
|
static setAll(options, prefs = false) {
|
||||||
if (init) {
|
let events;
|
||||||
if (this.get("disablePreferences")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const name in userOptions) {
|
|
||||||
if (compatibilityParams[name] !== undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
console.warn("setAll: The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option in order to prevent that.');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const name in options) {
|
for (const name in options) {
|
||||||
userOptions[name] = options[name];
|
const defaultOpt = defaultOptions[name],
|
||||||
|
userOpt = options[name];
|
||||||
|
if (!defaultOpt || !(typeof userOpt === typeof defaultOpt.value || Type[(typeof userOpt).toUpperCase()] & defaultOpt.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
kind
|
||||||
|
} = defaultOpt;
|
||||||
|
if (prefs && !(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) {
|
||||||
|
(events ||= new Map()).set(name, userOpt);
|
||||||
|
}
|
||||||
|
userOptions.set(name, userOpt);
|
||||||
|
}
|
||||||
|
if (events) {
|
||||||
|
for (const [name, value] of events) {
|
||||||
|
this.eventBus.dispatch(name.toLowerCase(), {
|
||||||
|
source: this,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static remove(name) {
|
}
|
||||||
delete userOptions[name];
|
{
|
||||||
const val = compatibilityParams[name];
|
AppOptions._checkDisablePreferences = () => {
|
||||||
if (val !== undefined) {
|
if (AppOptions.get("disablePreferences")) {
|
||||||
userOptions[name] = val;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
for (const [name] of userOptions) {
|
||||||
|
if (compatParams.has(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.warn("The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option to prevent that.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./web/pdf_link_service.js
|
;// CONCATENATED MODULE: ./web/pdf_link_service.js
|
||||||
@@ -1171,26 +1233,27 @@ class PDFLinkService {
|
|||||||
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
|
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const argsLen = args.length;
|
||||||
let allowNull = true;
|
let allowNull = true;
|
||||||
switch (zoom.name) {
|
switch (zoom.name) {
|
||||||
case "XYZ":
|
case "XYZ":
|
||||||
if (args.length !== 3) {
|
if (argsLen < 2 || argsLen > 3) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Fit":
|
case "Fit":
|
||||||
case "FitB":
|
case "FitB":
|
||||||
return args.length === 0;
|
return argsLen === 0;
|
||||||
case "FitH":
|
case "FitH":
|
||||||
case "FitBH":
|
case "FitBH":
|
||||||
case "FitV":
|
case "FitV":
|
||||||
case "FitBV":
|
case "FitBV":
|
||||||
if (args.length !== 1) {
|
if (argsLen > 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "FitR":
|
case "FitR":
|
||||||
if (args.length !== 4) {
|
if (argsLen !== 4) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
allowNull = false;
|
allowNull = false;
|
||||||
@@ -1240,7 +1303,6 @@ const {
|
|||||||
noContextMenu,
|
noContextMenu,
|
||||||
normalizeUnicode,
|
normalizeUnicode,
|
||||||
OPS,
|
OPS,
|
||||||
Outliner,
|
|
||||||
PasswordResponses,
|
PasswordResponses,
|
||||||
PDFDataRangeTransport,
|
PDFDataRangeTransport,
|
||||||
PDFDateString,
|
PDFDateString,
|
||||||
@@ -1248,12 +1310,10 @@ const {
|
|||||||
PermissionFlag,
|
PermissionFlag,
|
||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
renderTextLayer,
|
|
||||||
setLayerDimensions,
|
setLayerDimensions,
|
||||||
shadow,
|
shadow,
|
||||||
TextLayer,
|
TextLayer,
|
||||||
UnexpectedResponseException,
|
UnexpectedResponseException,
|
||||||
updateTextLayer,
|
|
||||||
Util,
|
Util,
|
||||||
VerbosityLevel,
|
VerbosityLevel,
|
||||||
version,
|
version,
|
||||||
@@ -1401,40 +1461,28 @@ class BaseExternalServices {
|
|||||||
updateEditorStates(data) {
|
updateEditorStates(data) {
|
||||||
throw new Error("Not implemented: updateEditorStates");
|
throw new Error("Not implemented: updateEditorStates");
|
||||||
}
|
}
|
||||||
async getNimbusExperimentData() {}
|
|
||||||
async getGlobalEventNames() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
dispatchGlobalEvent(_event) {}
|
dispatchGlobalEvent(_event) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./web/preferences.js
|
;// CONCATENATED MODULE: ./web/preferences.js
|
||||||
|
|
||||||
class BasePreferences {
|
class BasePreferences {
|
||||||
#browserDefaults = Object.freeze({
|
|
||||||
canvasMaxAreaInBytes: -1,
|
|
||||||
isInAutomation: false,
|
|
||||||
supportsCaretBrowsingMode: false,
|
|
||||||
supportsDocumentFonts: true,
|
|
||||||
supportsIntegratedFind: false,
|
|
||||||
supportsMouseWheelZoomCtrlKey: true,
|
|
||||||
supportsMouseWheelZoomMetaKey: true,
|
|
||||||
supportsPinchToZoom: true
|
|
||||||
});
|
|
||||||
#defaults = Object.freeze({
|
#defaults = Object.freeze({
|
||||||
|
altTextLearnMoreUrl: "",
|
||||||
annotationEditorMode: 0,
|
annotationEditorMode: 0,
|
||||||
annotationMode: 2,
|
annotationMode: 2,
|
||||||
cursorToolOnLoad: 0,
|
cursorToolOnLoad: 0,
|
||||||
defaultZoomDelay: 400,
|
defaultZoomDelay: 400,
|
||||||
defaultZoomValue: "",
|
defaultZoomValue: "",
|
||||||
disablePageLabels: false,
|
disablePageLabels: false,
|
||||||
|
enableAltText: false,
|
||||||
|
enableGuessAltText: true,
|
||||||
enableHighlightEditor: false,
|
enableHighlightEditor: false,
|
||||||
enableHighlightFloatingButton: false,
|
enableHighlightFloatingButton: false,
|
||||||
enableML: false,
|
|
||||||
enablePermissions: false,
|
enablePermissions: false,
|
||||||
enablePrintAutoRotate: true,
|
enablePrintAutoRotate: true,
|
||||||
enableScripting: true,
|
enableScripting: true,
|
||||||
enableStampEditor: true,
|
enableUpdatedAddImage: false,
|
||||||
externalLinkTarget: 0,
|
externalLinkTarget: 0,
|
||||||
highlightEditorColors: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
|
highlightEditorColors: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
|
||||||
historyUpdateUrl: false,
|
historyUpdateUrl: false,
|
||||||
@@ -1456,7 +1504,6 @@ class BasePreferences {
|
|||||||
enableXfa: true,
|
enableXfa: true,
|
||||||
viewerCssTheme: 0
|
viewerCssTheme: 0
|
||||||
});
|
});
|
||||||
#prefs = Object.create(null);
|
|
||||||
#initializedPromise = null;
|
#initializedPromise = null;
|
||||||
constructor() {
|
constructor() {
|
||||||
if (this.constructor === BasePreferences) {
|
if (this.constructor === BasePreferences) {
|
||||||
@@ -1466,16 +1513,13 @@ class BasePreferences {
|
|||||||
browserPrefs,
|
browserPrefs,
|
||||||
prefs
|
prefs
|
||||||
}) => {
|
}) => {
|
||||||
const options = Object.create(null);
|
if (AppOptions._checkDisablePreferences()) {
|
||||||
for (const [name, val] of Object.entries(this.#browserDefaults)) {
|
return;
|
||||||
const prefVal = browserPrefs?.[name];
|
|
||||||
options[name] = typeof prefVal === typeof val ? prefVal : val;
|
|
||||||
}
|
}
|
||||||
for (const [name, val] of Object.entries(this.#defaults)) {
|
AppOptions.setAll({
|
||||||
const prefVal = prefs?.[name];
|
...browserPrefs,
|
||||||
options[name] = this.#prefs[name] = typeof prefVal === typeof val ? prefVal : val;
|
...prefs
|
||||||
}
|
}, true);
|
||||||
AppOptions.setAll(options, true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async _writeToStorage(prefObj) {
|
async _writeToStorage(prefObj) {
|
||||||
@@ -1484,58 +1528,21 @@ class BasePreferences {
|
|||||||
async _readFromStorage(prefObj) {
|
async _readFromStorage(prefObj) {
|
||||||
throw new Error("Not implemented: _readFromStorage");
|
throw new Error("Not implemented: _readFromStorage");
|
||||||
}
|
}
|
||||||
#updatePref({
|
|
||||||
name,
|
|
||||||
value
|
|
||||||
}) {
|
|
||||||
throw new Error("Not implemented: #updatePref");
|
|
||||||
}
|
|
||||||
async reset() {
|
async reset() {
|
||||||
await this.#initializedPromise;
|
await this.#initializedPromise;
|
||||||
const oldPrefs = structuredClone(this.#prefs);
|
AppOptions.setAll(this.#defaults, true);
|
||||||
this.#prefs = Object.create(null);
|
await this._writeToStorage(this.#defaults);
|
||||||
try {
|
|
||||||
await this._writeToStorage(this.#defaults);
|
|
||||||
} catch (reason) {
|
|
||||||
this.#prefs = oldPrefs;
|
|
||||||
throw reason;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async set(name, value) {
|
async set(name, value) {
|
||||||
await this.#initializedPromise;
|
await this.#initializedPromise;
|
||||||
const defaultValue = this.#defaults[name],
|
AppOptions.setAll({
|
||||||
oldPrefs = structuredClone(this.#prefs);
|
[name]: value
|
||||||
if (defaultValue === undefined) {
|
}, true);
|
||||||
throw new Error(`Set preference: "${name}" is undefined.`);
|
await this._writeToStorage(AppOptions.getAll(OptionKind.PREFERENCE));
|
||||||
} else if (value === undefined) {
|
|
||||||
throw new Error("Set preference: no value is specified.");
|
|
||||||
}
|
|
||||||
const valueType = typeof value,
|
|
||||||
defaultType = typeof defaultValue;
|
|
||||||
if (valueType !== defaultType) {
|
|
||||||
if (valueType === "number" && defaultType === "string") {
|
|
||||||
value = value.toString();
|
|
||||||
} else {
|
|
||||||
throw new Error(`Set preference: "${value}" is a ${valueType}, expected a ${defaultType}.`);
|
|
||||||
}
|
|
||||||
} else if (valueType === "number" && !Number.isInteger(value)) {
|
|
||||||
throw new Error(`Set preference: "${value}" must be an integer.`);
|
|
||||||
}
|
|
||||||
this.#prefs[name] = value;
|
|
||||||
try {
|
|
||||||
await this._writeToStorage(this.#prefs);
|
|
||||||
} catch (reason) {
|
|
||||||
this.#prefs = oldPrefs;
|
|
||||||
throw reason;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async get(name) {
|
async get(name) {
|
||||||
await this.#initializedPromise;
|
await this.#initializedPromise;
|
||||||
const defaultValue = this.#defaults[name];
|
return AppOptions.get(name);
|
||||||
if (defaultValue === undefined) {
|
|
||||||
throw new Error(`Get preference: "${name}" is undefined.`);
|
|
||||||
}
|
|
||||||
return this.#prefs[name] ?? defaultValue;
|
|
||||||
}
|
}
|
||||||
get initializedPromise() {
|
get initializedPromise() {
|
||||||
return this.#initializedPromise;
|
return this.#initializedPromise;
|
||||||
@@ -3098,13 +3105,19 @@ class Preferences extends BasePreferences {
|
|||||||
}
|
}
|
||||||
class ExternalServices extends BaseExternalServices {
|
class ExternalServices extends BaseExternalServices {
|
||||||
async createL10n() {
|
async createL10n() {
|
||||||
return new genericl10n_GenericL10n(AppOptions.get("locale"));
|
return new genericl10n_GenericL10n(AppOptions.get("localeProperties")?.lang);
|
||||||
}
|
}
|
||||||
createScripting() {
|
createScripting() {
|
||||||
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class MLManager {
|
class MLManager {
|
||||||
|
async isEnabledFor(_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async deleteModel(_service) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
async guess() {
|
async guess() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -8411,6 +8424,9 @@ class AnnotationLayerBuilder {
|
|||||||
}
|
}
|
||||||
this.div.hidden = true;
|
this.div.hidden = true;
|
||||||
}
|
}
|
||||||
|
hasEditableAnnotations() {
|
||||||
|
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||||
|
}
|
||||||
#updatePresentationModeState(state) {
|
#updatePresentationModeState(state) {
|
||||||
if (!this.div) {
|
if (!this.div) {
|
||||||
return;
|
return;
|
||||||
@@ -9142,6 +9158,7 @@ class PDFPageView {
|
|||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
#enableHWA = false;
|
#enableHWA = false;
|
||||||
#hasRestrictedScaling = false;
|
#hasRestrictedScaling = false;
|
||||||
|
#isEditing = false;
|
||||||
#layerProperties = null;
|
#layerProperties = null;
|
||||||
#loadingId = null;
|
#loadingId = null;
|
||||||
#previousRotation = null;
|
#previousRotation = null;
|
||||||
@@ -9296,6 +9313,9 @@ class PDFPageView {
|
|||||||
this.reset();
|
this.reset();
|
||||||
this.pdfPage?.cleanup();
|
this.pdfPage?.cleanup();
|
||||||
}
|
}
|
||||||
|
hasEditableAnnotations() {
|
||||||
|
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||||
|
}
|
||||||
get _textHighlighter() {
|
get _textHighlighter() {
|
||||||
return shadow(this, "_textHighlighter", new TextHighlighter({
|
return shadow(this, "_textHighlighter", new TextHighlighter({
|
||||||
pageIndex: this.id - 1,
|
pageIndex: this.id - 1,
|
||||||
@@ -9472,6 +9492,19 @@ class PDFPageView {
|
|||||||
this._resetZoomLayer();
|
this._resetZoomLayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toggleEditingMode(isEditing) {
|
||||||
|
if (!this.hasEditableAnnotations()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isEditing = isEditing;
|
||||||
|
this.reset({
|
||||||
|
keepZoomLayer: true,
|
||||||
|
keepAnnotationLayer: true,
|
||||||
|
keepAnnotationEditorLayer: true,
|
||||||
|
keepXfaLayer: true,
|
||||||
|
keepTextLayer: true
|
||||||
|
});
|
||||||
|
}
|
||||||
update({
|
update({
|
||||||
scale = 0,
|
scale = 0,
|
||||||
rotation = null,
|
rotation = null,
|
||||||
@@ -9822,7 +9855,8 @@ class PDFPageView {
|
|||||||
annotationMode: this.#annotationMode,
|
annotationMode: this.#annotationMode,
|
||||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
pageColors
|
pageColors,
|
||||||
|
isEditing: this.#isEditing
|
||||||
};
|
};
|
||||||
const renderTask = this.renderTask = pdfPage.render(renderContext);
|
const renderTask = this.renderTask = pdfPage.render(renderContext);
|
||||||
renderTask.onContinue = renderContinueCallback;
|
renderTask.onContinue = renderContinueCallback;
|
||||||
@@ -9982,8 +10016,11 @@ class PDFViewer {
|
|||||||
#enableHWA = false;
|
#enableHWA = false;
|
||||||
#enableHighlightFloatingButton = false;
|
#enableHighlightFloatingButton = false;
|
||||||
#enablePermissions = false;
|
#enablePermissions = false;
|
||||||
|
#enableUpdatedAddImage = false;
|
||||||
#eventAbortController = null;
|
#eventAbortController = null;
|
||||||
#mlManager = null;
|
#mlManager = null;
|
||||||
|
#onPageRenderedCallback = null;
|
||||||
|
#switchAnnotationEditorModeTimeoutId = null;
|
||||||
#getAllTextInProgress = false;
|
#getAllTextInProgress = false;
|
||||||
#hiddenCopyElement = null;
|
#hiddenCopyElement = null;
|
||||||
#interruptCopyCondition = false;
|
#interruptCopyCondition = false;
|
||||||
@@ -9993,7 +10030,7 @@ class PDFViewer {
|
|||||||
#scaleTimeoutId = null;
|
#scaleTimeoutId = null;
|
||||||
#textLayerMode = TextLayerMode.ENABLE;
|
#textLayerMode = TextLayerMode.ENABLE;
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const viewerVersion = "4.4.168";
|
const viewerVersion = "4.5.136";
|
||||||
if (version !== viewerVersion) {
|
if (version !== viewerVersion) {
|
||||||
throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`);
|
throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`);
|
||||||
}
|
}
|
||||||
@@ -10020,6 +10057,7 @@ class PDFViewer {
|
|||||||
this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE;
|
this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE;
|
||||||
this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null;
|
this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null;
|
||||||
this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true;
|
this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true;
|
||||||
|
this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true;
|
||||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||||
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
|
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
|
||||||
this.removePageBorders = options.removePageBorders || false;
|
this.removePageBorders = options.removePageBorders || false;
|
||||||
@@ -10425,7 +10463,7 @@ class PDFViewer {
|
|||||||
if (pdfDocument.isPureXfa) {
|
if (pdfDocument.isPureXfa) {
|
||||||
console.warn("Warning: XFA-editing is not implemented.");
|
console.warn("Warning: XFA-editing is not implemented.");
|
||||||
} else if (isValidAnnotationEditorMode(mode)) {
|
} else if (isValidAnnotationEditorMode(mode)) {
|
||||||
this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#mlManager);
|
this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#mlManager);
|
||||||
eventBus.dispatch("annotationeditoruimanager", {
|
eventBus.dispatch("annotationeditoruimanager", {
|
||||||
source: this,
|
source: this,
|
||||||
uiManager: this.#annotationEditorUIManager
|
uiManager: this.#annotationEditorUIManager
|
||||||
@@ -10584,6 +10622,7 @@ class PDFViewer {
|
|||||||
this.viewer.removeAttribute("lang");
|
this.viewer.removeAttribute("lang");
|
||||||
this.#hiddenCopyElement?.remove();
|
this.#hiddenCopyElement?.remove();
|
||||||
this.#hiddenCopyElement = null;
|
this.#hiddenCopyElement = null;
|
||||||
|
this.#cleanupSwitchAnnotationEditorMode();
|
||||||
}
|
}
|
||||||
#ensurePageViewVisible() {
|
#ensurePageViewVisible() {
|
||||||
if (this._scrollMode !== ScrollMode.PAGE) {
|
if (this._scrollMode !== ScrollMode.PAGE) {
|
||||||
@@ -10956,6 +10995,34 @@ class PDFViewer {
|
|||||||
location: this._location
|
location: this._location
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
#switchToEditAnnotationMode() {
|
||||||
|
const visible = this._getVisiblePages();
|
||||||
|
const pagesToRefresh = [];
|
||||||
|
const {
|
||||||
|
ids,
|
||||||
|
views
|
||||||
|
} = visible;
|
||||||
|
for (const page of views) {
|
||||||
|
const {
|
||||||
|
view
|
||||||
|
} = page;
|
||||||
|
if (!view.hasEditableAnnotations()) {
|
||||||
|
ids.delete(view.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pagesToRefresh.push(page);
|
||||||
|
}
|
||||||
|
if (pagesToRefresh.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.renderingQueue.renderHighestPriority({
|
||||||
|
first: pagesToRefresh[0],
|
||||||
|
last: pagesToRefresh.at(-1),
|
||||||
|
views: pagesToRefresh,
|
||||||
|
ids
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
containsElement(element) {
|
containsElement(element) {
|
||||||
return this.container.contains(element);
|
return this.container.contains(element);
|
||||||
}
|
}
|
||||||
@@ -11388,6 +11455,16 @@ class PDFViewer {
|
|||||||
get containerTopLeft() {
|
get containerTopLeft() {
|
||||||
return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft];
|
return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft];
|
||||||
}
|
}
|
||||||
|
#cleanupSwitchAnnotationEditorMode() {
|
||||||
|
if (this.#onPageRenderedCallback) {
|
||||||
|
this.eventBus._off("pagerendered", this.#onPageRenderedCallback);
|
||||||
|
this.#onPageRenderedCallback = null;
|
||||||
|
}
|
||||||
|
if (this.#switchAnnotationEditorModeTimeoutId !== null) {
|
||||||
|
clearTimeout(this.#switchAnnotationEditorModeTimeoutId);
|
||||||
|
this.#switchAnnotationEditorModeTimeoutId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
get annotationEditorMode() {
|
get annotationEditorMode() {
|
||||||
return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE;
|
return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE;
|
||||||
}
|
}
|
||||||
@@ -11408,12 +11485,47 @@ class PDFViewer {
|
|||||||
if (!this.pdfDocument) {
|
if (!this.pdfDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#annotationEditorMode = mode;
|
const {
|
||||||
this.eventBus.dispatch("annotationeditormodechanged", {
|
eventBus
|
||||||
source: this,
|
} = this;
|
||||||
mode
|
const updater = () => {
|
||||||
});
|
this.#cleanupSwitchAnnotationEditorMode();
|
||||||
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
|
this.#annotationEditorMode = mode;
|
||||||
|
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
|
||||||
|
eventBus.dispatch("annotationeditormodechanged", {
|
||||||
|
source: this,
|
||||||
|
mode
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (mode === AnnotationEditorType.NONE || this.#annotationEditorMode === AnnotationEditorType.NONE) {
|
||||||
|
const isEditing = mode !== AnnotationEditorType.NONE;
|
||||||
|
if (!isEditing) {
|
||||||
|
this.pdfDocument.annotationStorage.resetModifiedIds();
|
||||||
|
}
|
||||||
|
for (const pageView of this._pages) {
|
||||||
|
pageView.toggleEditingMode(isEditing);
|
||||||
|
}
|
||||||
|
const idsToRefresh = this.#switchToEditAnnotationMode();
|
||||||
|
if (isEditing && idsToRefresh) {
|
||||||
|
this.#cleanupSwitchAnnotationEditorMode();
|
||||||
|
this.#onPageRenderedCallback = ({
|
||||||
|
pageNumber
|
||||||
|
}) => {
|
||||||
|
idsToRefresh.delete(pageNumber);
|
||||||
|
if (idsToRefresh.size === 0) {
|
||||||
|
this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
signal
|
||||||
|
} = this.#eventAbortController;
|
||||||
|
eventBus._on("pagerendered", this.#onPageRenderedCallback, {
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updater();
|
||||||
}
|
}
|
||||||
set annotationEditorParams({
|
set annotationEditorParams({
|
||||||
type,
|
type,
|
||||||
@@ -11721,7 +11833,7 @@ class SecondaryToolbar {
|
|||||||
|
|
||||||
class Toolbar {
|
class Toolbar {
|
||||||
#opts;
|
#opts;
|
||||||
constructor(options, eventBus) {
|
constructor(options, eventBus, toolbarDensity = 0) {
|
||||||
this.#opts = options;
|
this.#opts = options;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
const buttons = [{
|
const buttons = [{
|
||||||
@@ -11806,8 +11918,13 @@ class Toolbar {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this));
|
||||||
|
this.#updateToolbarDensity({
|
||||||
|
value: toolbarDensity
|
||||||
|
});
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
#updateToolbarDensity() {}
|
||||||
#setAnnotationEditorUIManager(uiManager, parentContainer) {
|
#setAnnotationEditorUIManager(uiManager, parentContainer) {
|
||||||
const colorPicker = new ColorPicker({
|
const colorPicker = new ColorPicker({
|
||||||
uiManager
|
uiManager
|
||||||
@@ -12078,7 +12195,6 @@ class ViewHistory {
|
|||||||
|
|
||||||
|
|
||||||
const FORCE_PAGES_LOADED_TIMEOUT = 10000;
|
const FORCE_PAGES_LOADED_TIMEOUT = 10000;
|
||||||
const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000;
|
|
||||||
const ViewOnLoad = {
|
const ViewOnLoad = {
|
||||||
UNKNOWN: -1,
|
UNKNOWN: -1,
|
||||||
PREVIOUS: 0,
|
PREVIOUS: 0,
|
||||||
@@ -12110,18 +12226,17 @@ const PDFViewerApplication = {
|
|||||||
store: null,
|
store: null,
|
||||||
downloadManager: null,
|
downloadManager: null,
|
||||||
overlayManager: null,
|
overlayManager: null,
|
||||||
preferences: null,
|
preferences: new Preferences(),
|
||||||
toolbar: null,
|
toolbar: null,
|
||||||
secondaryToolbar: null,
|
secondaryToolbar: null,
|
||||||
eventBus: null,
|
eventBus: null,
|
||||||
l10n: null,
|
l10n: null,
|
||||||
annotationEditorParams: null,
|
annotationEditorParams: null,
|
||||||
isInitialViewSet: false,
|
isInitialViewSet: false,
|
||||||
downloadComplete: false,
|
|
||||||
isViewerEmbedded: window.parent !== window,
|
isViewerEmbedded: window.parent !== window,
|
||||||
url: "",
|
url: "",
|
||||||
baseUrl: "",
|
baseUrl: "",
|
||||||
_allowedGlobalEventsPromise: null,
|
mlManager: null,
|
||||||
_downloadUrl: "",
|
_downloadUrl: "",
|
||||||
_eventBusAbortController: null,
|
_eventBusAbortController: null,
|
||||||
_windowAbortController: null,
|
_windowAbortController: null,
|
||||||
@@ -12141,11 +12256,9 @@ const PDFViewerApplication = {
|
|||||||
_printAnnotationStoragePromise: null,
|
_printAnnotationStoragePromise: null,
|
||||||
_touchInfo: null,
|
_touchInfo: null,
|
||||||
_isCtrlKeyDown: false,
|
_isCtrlKeyDown: false,
|
||||||
_nimbusDataPromise: null,
|
|
||||||
_caretBrowsing: null,
|
_caretBrowsing: null,
|
||||||
_isScrolling: false,
|
_isScrolling: false,
|
||||||
async initialize(appConfig) {
|
async initialize(appConfig) {
|
||||||
let l10nPromise;
|
|
||||||
this.appConfig = appConfig;
|
this.appConfig = appConfig;
|
||||||
try {
|
try {
|
||||||
await this.preferences.initializedPromise;
|
await this.preferences.initializedPromise;
|
||||||
@@ -12167,8 +12280,7 @@ const PDFViewerApplication = {
|
|||||||
if (mode) {
|
if (mode) {
|
||||||
document.documentElement.classList.add(mode);
|
document.documentElement.classList.add(mode);
|
||||||
}
|
}
|
||||||
l10nPromise = this.externalServices.createL10n();
|
this.l10n = await this.externalServices.createL10n();
|
||||||
this.l10n = await l10nPromise;
|
|
||||||
document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
|
document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
|
||||||
this.l10n.translate(appConfig.appContainer || document.documentElement);
|
this.l10n.translate(appConfig.appContainer || document.documentElement);
|
||||||
if (this.isViewerEmbedded && AppOptions.get("externalLinkTarget") === LinkTarget.NONE) {
|
if (this.isViewerEmbedded && AppOptions.get("externalLinkTarget") === LinkTarget.NONE) {
|
||||||
@@ -12257,7 +12369,9 @@ const PDFViewerApplication = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (params.has("locale")) {
|
if (params.has("locale")) {
|
||||||
AppOptions.set("locale", params.get("locale"));
|
AppOptions.set("localeProperties", {
|
||||||
|
lang: params.get("locale")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async _initializeViewerComponents() {
|
async _initializeViewerComponents() {
|
||||||
@@ -12318,6 +12432,7 @@ const PDFViewerApplication = {
|
|||||||
annotationEditorMode,
|
annotationEditorMode,
|
||||||
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
||||||
enableHighlightFloatingButton: AppOptions.get("enableHighlightFloatingButton"),
|
enableHighlightFloatingButton: AppOptions.get("enableHighlightFloatingButton"),
|
||||||
|
enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"),
|
||||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||||
@@ -12355,9 +12470,6 @@ const PDFViewerApplication = {
|
|||||||
}
|
}
|
||||||
if (appConfig.annotationEditorParams) {
|
if (appConfig.annotationEditorParams) {
|
||||||
if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
|
if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
|
||||||
if (AppOptions.get("enableStampEditor")) {
|
|
||||||
appConfig.toolbar?.editorStampButton?.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
|
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
|
||||||
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
|
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
|
||||||
editorHighlightButton.hidden = false;
|
editorHighlightButton.hidden = false;
|
||||||
@@ -12380,7 +12492,7 @@ const PDFViewerApplication = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (appConfig.toolbar) {
|
if (appConfig.toolbar) {
|
||||||
this.toolbar = new Toolbar(appConfig.toolbar, eventBus);
|
this.toolbar = new Toolbar(appConfig.toolbar, eventBus, AppOptions.get("toolbarDensity"));
|
||||||
}
|
}
|
||||||
if (appConfig.secondaryToolbar) {
|
if (appConfig.secondaryToolbar) {
|
||||||
this.secondaryToolbar = new SecondaryToolbar(appConfig.secondaryToolbar, eventBus);
|
this.secondaryToolbar = new SecondaryToolbar(appConfig.secondaryToolbar, eventBus);
|
||||||
@@ -12437,7 +12549,6 @@ const PDFViewerApplication = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async run(config) {
|
async run(config) {
|
||||||
this.preferences = new Preferences();
|
|
||||||
await this.initialize(config);
|
await this.initialize(config);
|
||||||
const {
|
const {
|
||||||
appConfig,
|
appConfig,
|
||||||
@@ -12514,9 +12625,6 @@ const PDFViewerApplication = {
|
|||||||
get externalServices() {
|
get externalServices() {
|
||||||
return shadow(this, "externalServices", new ExternalServices());
|
return shadow(this, "externalServices", new ExternalServices());
|
||||||
},
|
},
|
||||||
get mlManager() {
|
|
||||||
return shadow(this, "mlManager", AppOptions.get("enableML") === true ? new MLManager() : null);
|
|
||||||
},
|
|
||||||
get initialized() {
|
get initialized() {
|
||||||
return this._initializedCapability.settled;
|
return this._initializedCapability.settled;
|
||||||
},
|
},
|
||||||
@@ -12597,12 +12705,10 @@ const PDFViewerApplication = {
|
|||||||
let title = pdfjs_getPdfFilenameFromUrl(url, "");
|
let title = pdfjs_getPdfFilenameFromUrl(url, "");
|
||||||
if (!title) {
|
if (!title) {
|
||||||
try {
|
try {
|
||||||
title = decodeURIComponent(getFilenameFromUrl(url)) || url;
|
title = decodeURIComponent(getFilenameFromUrl(url));
|
||||||
} catch {
|
} catch {}
|
||||||
title = url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.setTitle(title);
|
this.setTitle(title || url);
|
||||||
},
|
},
|
||||||
setTitle(title = this._title) {
|
setTitle(title = this._title) {
|
||||||
this._title = title;
|
this._title = title;
|
||||||
@@ -12648,7 +12754,6 @@ const PDFViewerApplication = {
|
|||||||
this.pdfLinkService.externalLinkEnabled = true;
|
this.pdfLinkService.externalLinkEnabled = true;
|
||||||
this.store = null;
|
this.store = null;
|
||||||
this.isInitialViewSet = false;
|
this.isInitialViewSet = false;
|
||||||
this.downloadComplete = false;
|
|
||||||
this.url = "";
|
this.url = "";
|
||||||
this.baseUrl = "";
|
this.baseUrl = "";
|
||||||
this._downloadUrl = "";
|
this._downloadUrl = "";
|
||||||
@@ -12724,9 +12829,7 @@ const PDFViewerApplication = {
|
|||||||
async download(options = {}) {
|
async download(options = {}) {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
if (this.downloadComplete) {
|
data = await this.pdfDocument.getData();
|
||||||
data = await this.pdfDocument.getData();
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||||
},
|
},
|
||||||
@@ -12793,11 +12896,8 @@ const PDFViewerApplication = {
|
|||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
progress(level) {
|
progress(level) {
|
||||||
if (!this.loadingBar || this.downloadComplete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const percent = Math.round(level * 100);
|
const percent = Math.round(level * 100);
|
||||||
if (percent <= this.loadingBar.percent) {
|
if (!this.loadingBar || percent <= this.loadingBar.percent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadingBar.percent = percent;
|
this.loadingBar.percent = percent;
|
||||||
@@ -12811,7 +12911,6 @@ const PDFViewerApplication = {
|
|||||||
length
|
length
|
||||||
}) => {
|
}) => {
|
||||||
this._contentLength = length;
|
this._contentLength = length;
|
||||||
this.downloadComplete = true;
|
|
||||||
this.loadingBar?.hide();
|
this.loadingBar?.hide();
|
||||||
firstPagePromise.then(() => {
|
firstPagePromise.then(() => {
|
||||||
this.eventBus.dispatch("documentloaded", {
|
this.eventBus.dispatch("documentloaded", {
|
||||||
@@ -13413,9 +13512,6 @@ const PDFViewerApplication = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
addWindowResolutionChange();
|
addWindowResolutionChange();
|
||||||
window.addEventListener("visibilitychange", webViewerVisibilityChange, {
|
|
||||||
signal
|
|
||||||
});
|
|
||||||
window.addEventListener("wheel", webViewerWheel, {
|
window.addEventListener("wheel", webViewerWheel, {
|
||||||
passive: false,
|
passive: false,
|
||||||
signal
|
signal
|
||||||
@@ -13730,7 +13826,7 @@ function webViewerHashchange(evt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
/*var webViewerFileInputChange = function (evt) {
|
var webViewerFileInputChange = function (evt) {
|
||||||
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -13742,7 +13838,7 @@ function webViewerHashchange(evt) {
|
|||||||
};
|
};
|
||||||
var webViewerOpenFile = function (evt) {
|
var webViewerOpenFile = function (evt) {
|
||||||
PDFViewerApplication._openFileInput?.click();
|
PDFViewerApplication._openFileInput?.click();
|
||||||
};*/
|
};
|
||||||
}
|
}
|
||||||
function webViewerPresentationMode() {
|
function webViewerPresentationMode() {
|
||||||
PDFViewerApplication.requestPresentationMode();
|
PDFViewerApplication.requestPresentationMode();
|
||||||
@@ -13876,20 +13972,6 @@ function webViewerPageChanging({
|
|||||||
function webViewerResolutionChange(evt) {
|
function webViewerResolutionChange(evt) {
|
||||||
PDFViewerApplication.pdfViewer.refresh();
|
PDFViewerApplication.pdfViewer.refresh();
|
||||||
}
|
}
|
||||||
function webViewerVisibilityChange(evt) {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
setZoomDisabledTimeout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let zoomDisabledTimeout = null;
|
|
||||||
function setZoomDisabledTimeout() {
|
|
||||||
if (zoomDisabledTimeout) {
|
|
||||||
clearTimeout(zoomDisabledTimeout);
|
|
||||||
}
|
|
||||||
zoomDisabledTimeout = setTimeout(function () {
|
|
||||||
zoomDisabledTimeout = null;
|
|
||||||
}, WHEEL_ZOOM_DISABLED_TIMEOUT);
|
|
||||||
}
|
|
||||||
function webViewerWheel(evt) {
|
function webViewerWheel(evt) {
|
||||||
const {
|
const {
|
||||||
pdfViewer,
|
pdfViewer,
|
||||||
@@ -13907,7 +13989,7 @@ function webViewerWheel(evt) {
|
|||||||
const origin = [evt.clientX, evt.clientY];
|
const origin = [evt.clientX, evt.clientY];
|
||||||
if (isPinchToZoom || evt.ctrlKey && supportsMouseWheelZoomCtrlKey || evt.metaKey && supportsMouseWheelZoomMetaKey) {
|
if (isPinchToZoom || evt.ctrlKey && supportsMouseWheelZoomCtrlKey || evt.metaKey && supportsMouseWheelZoomMetaKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (PDFViewerApplication._isScrolling || zoomDisabledTimeout || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) {
|
if (PDFViewerApplication._isScrolling || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPinchToZoom && supportsPinchToZoom) {
|
if (isPinchToZoom && supportsPinchToZoom) {
|
||||||
@@ -14335,14 +14417,20 @@ function webViewerReportTelemetry({
|
|||||||
}) {
|
}) {
|
||||||
PDFViewerApplication.externalServices.reportTelemetry(details);
|
PDFViewerApplication.externalServices.reportTelemetry(details);
|
||||||
}
|
}
|
||||||
|
function webViewerSetPreference({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
}) {
|
||||||
|
PDFViewerApplication.preferences.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./web/viewer.js
|
;// CONCATENATED MODULE: ./web/viewer.js
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const pdfjsVersion = "4.4.168";
|
const pdfjsVersion = "4.5.136";
|
||||||
const pdfjsBuild = "19fbc8998";
|
const pdfjsBuild = "3a21f03b0";
|
||||||
const AppConstants = {
|
const AppConstants = {
|
||||||
LinkTarget: LinkTarget,
|
LinkTarget: LinkTarget,
|
||||||
RenderingStates: RenderingStates,
|
RenderingStates: RenderingStates,
|
||||||
|
|||||||
@@ -130,8 +130,13 @@ $(".container-fluid").bind('drop', function (e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (dt.files.length) {
|
if (dt.files.length) {
|
||||||
$("#btn-upload")[0].files = dt.files;
|
if($("#btn-upload-format").length) {
|
||||||
$("#form-upload").submit();
|
$("#btn-upload-format")[0].files = dt.files;
|
||||||
|
$("#form-upload-format").submit();
|
||||||
|
} else {
|
||||||
|
$("#btn-upload")[0].files = dt.files;
|
||||||
|
$("#form-upload").submit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -140,12 +145,25 @@ $("#btn-upload").change(function() {
|
|||||||
$("#form-upload").submit();
|
$("#form-upload").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn-upload-format").change(function() {
|
||||||
|
$("#form-upload-format").submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#form-upload").uploadprogress({
|
$("#form-upload").uploadprogress({
|
||||||
redirect_url: getPath() + "/", //"{{ url_for('web.index')}}",
|
redirect_url: getPath() + "/",
|
||||||
uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}",
|
uploadedMsg: $("#form-upload").data("message"),
|
||||||
modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}",
|
modalTitle: $("#form-upload").data("title"),
|
||||||
modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}",
|
modalFooter: $("#form-upload").data("footer"),
|
||||||
modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}"
|
modalTitleFailed: $("#form-upload").data("failed")
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#form-upload-format").uploadprogress({
|
||||||
|
redirect_url: getPath() + "/",
|
||||||
|
uploadedMsg: $("#form-upload-format").data("message"),
|
||||||
|
modalTitle: $("#form-upload-format").data("title"),
|
||||||
|
modalFooter: $("#form-upload-format").data("footer"),
|
||||||
|
modalTitleFailed: $("#form-upload-format").data("failed")
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -160,15 +178,21 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
$(".session").click(function() {
|
$(".session").click(function() {
|
||||||
window.sessionStorage.setItem("back", window.location.pathname);
|
window.sessionStorage.setItem("back", window.location.pathname);
|
||||||
|
window.sessionStorage.setItem("search", window.location.search);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#back").click(function() {
|
$("#back").click(function() {
|
||||||
var loc = sessionStorage.getItem("back");
|
var loc = sessionStorage.getItem("back");
|
||||||
|
var param = sessionStorage.getItem("search");
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
loc = $(this).data("back");
|
loc = $(this).data("back");
|
||||||
}
|
}
|
||||||
sessionStorage.removeItem("back");
|
sessionStorage.removeItem("back");
|
||||||
window.location.href = loc;
|
sessionStorage.removeItem("search");
|
||||||
|
if (param === null) {
|
||||||
|
param = "";
|
||||||
|
}
|
||||||
|
window.location.href = loc + param;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -598,6 +622,7 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#toggle_order_shelf").click(function() {
|
$("#toggle_order_shelf").click(function() {
|
||||||
|
$("#toggle_order_shelf").toggleClass("dummy");
|
||||||
$("#new").toggleClass("disabled");
|
$("#new").toggleClass("disabled");
|
||||||
$("#old").toggleClass("disabled");
|
$("#old").toggleClass("disabled");
|
||||||
$("#asc").toggleClass("disabled");
|
$("#asc").toggleClass("disabled");
|
||||||
@@ -606,9 +631,20 @@ $(function() {
|
|||||||
$("#auth_za").toggleClass("disabled");
|
$("#auth_za").toggleClass("disabled");
|
||||||
$("#pub_new").toggleClass("disabled");
|
$("#pub_new").toggleClass("disabled");
|
||||||
$("#pub_old").toggleClass("disabled");
|
$("#pub_old").toggleClass("disabled");
|
||||||
|
$("#shelf_new").toggleClass("disabled");
|
||||||
|
$("#shelf_old").toggleClass("disabled");
|
||||||
var alternative_text = $("#toggle_order_shelf").data('alt-text');
|
var alternative_text = $("#toggle_order_shelf").data('alt-text');
|
||||||
|
var status = $("#toggle_order_shelf").hasClass("dummy") ? "on" : "off";
|
||||||
$("#toggle_order_shelf").data('alt-text', $("#toggle_order_shelf").html());
|
$("#toggle_order_shelf").data('alt-text', $("#toggle_order_shelf").html());
|
||||||
$("#toggle_order_shelf").html(alternative_text);
|
$("#toggle_order_shelf").html(alternative_text);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/view",
|
||||||
|
data: "{\"shelf\": {\"man\": \"" + status + "\"}}",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btndeluser").click(function() {
|
$("#btndeluser").click(function() {
|
||||||
|
|||||||
23
cps/string_helper.py
Normal file
23
cps/string_helper.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2024 OzzieIsaacs
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def strip_whitespaces(text):
|
||||||
|
return re.sub("(^[\s\u200B-\u200D\ufeff]+)|([\s\u200B-\u200D\ufeff]+$)","", text)
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@ from cps.file_helper import get_temp_dir
|
|||||||
from cps.tasks.mail import TaskEmail
|
from cps.tasks.mail import TaskEmail
|
||||||
from cps import gdriveutils, helper
|
from cps import gdriveutils, helper
|
||||||
from cps.constants import SUPPORTED_CALIBRE_BINARIES
|
from cps.constants import SUPPORTED_CALIBRE_BINARIES
|
||||||
|
from cps.string_helper import strip_whitespaces
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class TaskConvert(CalibreTask):
|
|||||||
try:
|
try:
|
||||||
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
||||||
for email in self.ereader_mail.split(','):
|
for email in self.ereader_mail.split(','):
|
||||||
email = email.strip()
|
email = strip_whitespaces(email)
|
||||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||||
self.results["path"],
|
self.results["path"],
|
||||||
filename,
|
filename,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from cps.services import gmail
|
|||||||
from cps.embed_helper import do_calibre_export
|
from cps.embed_helper import do_calibre_export
|
||||||
from cps import logger, config
|
from cps import logger, config
|
||||||
from cps import gdriveutils
|
from cps import gdriveutils
|
||||||
|
from cps.string_helper import strip_whitespaces
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@@ -127,9 +128,9 @@ class TaskEmail(CalibreTask):
|
|||||||
try:
|
try:
|
||||||
# Parse out the address from the From line, and then the domain from that
|
# Parse out the address from the From line, and then the domain from that
|
||||||
from_email = parseaddr(self.settings["mail_from"])[1]
|
from_email = parseaddr(self.settings["mail_from"])[1]
|
||||||
msgid_domain = from_email.partition('@')[2].strip()
|
msgid_domain = strip_whitespaces(from_email.partition('@')[2])
|
||||||
# This can sometimes sneak through parseaddr if the input is malformed
|
# This can sometimes sneak through parseaddr if the input is malformed
|
||||||
msgid_domain = msgid_domain.rstrip('>').strip()
|
msgid_domain = strip_whitespaces(msgid_domain.rstrip('>'))
|
||||||
except Exception:
|
except Exception:
|
||||||
msgid_domain = ''
|
msgid_domain = ''
|
||||||
return msgid_domain or 'calibre-web.com'
|
return msgid_domain or 'calibre-web.com'
|
||||||
|
|||||||
@@ -20,14 +20,13 @@ import os
|
|||||||
from shutil import copyfile, copyfileobj
|
from shutil import copyfile, copyfileobj
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from .. import constants
|
from .. import constants
|
||||||
from cps import config, db, fs, gdriveutils, logger, ub
|
from cps import config, db, fs, gdriveutils, logger, ub
|
||||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import func, text, or_
|
from sqlalchemy import func, text, or_
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
use_IM = True
|
use_IM = True
|
||||||
@@ -36,7 +35,7 @@ except (ImportError, RuntimeError) as e:
|
|||||||
|
|
||||||
|
|
||||||
def get_resize_height(resolution):
|
def get_resize_height(resolution):
|
||||||
return int(225 * resolution)
|
return int(255 * resolution)
|
||||||
|
|
||||||
|
|
||||||
def get_resize_width(resolution, original_width, original_height):
|
def get_resize_width(resolution, original_width, original_height):
|
||||||
@@ -73,7 +72,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
self.cache = fs.FileSystem()
|
self.cache = fs.FileSystem()
|
||||||
self.resolutions = [
|
self.resolutions = [
|
||||||
constants.COVER_THUMBNAIL_SMALL,
|
constants.COVER_THUMBNAIL_SMALL,
|
||||||
constants.COVER_THUMBNAIL_MEDIUM
|
constants.COVER_THUMBNAIL_MEDIUM,
|
||||||
|
constants.COVER_THUMBNAIL_LARGE
|
||||||
]
|
]
|
||||||
|
|
||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
@@ -123,7 +123,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
.query(ub.Thumbnail) \
|
.query(ub.Thumbnail) \
|
||||||
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
|
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
|
||||||
.filter(ub.Thumbnail.entity_id == book_id) \
|
.filter(ub.Thumbnail.entity_id == book_id) \
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc))) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def create_book_cover_thumbnails(self, book):
|
def create_book_cover_thumbnails(self, book):
|
||||||
@@ -165,7 +165,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def update_book_cover_thumbnail(self, book, thumbnail):
|
def update_book_cover_thumbnail(self, book, thumbnail):
|
||||||
thumbnail.generated_at = datetime.utcnow()
|
thumbnail.generated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
@@ -197,9 +197,11 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
img.format = thumbnail.format
|
img.format = thumbnail.format
|
||||||
img.save(filename=filename)
|
img.save(filename=filename)
|
||||||
else:
|
else:
|
||||||
with open(filename, 'rb') as fd:
|
stream.seek(0)
|
||||||
|
with open(filename, 'wb') as fd:
|
||||||
copyfileobj(stream, fd)
|
copyfileobj(stream, fd)
|
||||||
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
# Bubble exception to calling function
|
# Bubble exception to calling function
|
||||||
self.log.debug('Error generating thumbnail file: ' + str(ex))
|
self.log.debug('Error generating thumbnail file: ' + str(ex))
|
||||||
@@ -322,12 +324,12 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
def get_series_thumbnails(self, series_id):
|
def get_series_thumbnails(self, series_id):
|
||||||
return self.app_db_session \
|
return (self.app_db_session
|
||||||
.query(ub.Thumbnail) \
|
.query(ub.Thumbnail)
|
||||||
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES) \
|
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES)
|
||||||
.filter(ub.Thumbnail.entity_id == series_id) \
|
.filter(ub.Thumbnail.entity_id == series_id)
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||||
.all()
|
.all())
|
||||||
|
|
||||||
def create_series_thumbnail(self, series, series_books, resolution):
|
def create_series_thumbnail(self, series, series_books, resolution):
|
||||||
thumbnail = ub.Thumbnail()
|
thumbnail = ub.Thumbnail()
|
||||||
@@ -346,7 +348,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def update_series_thumbnail(self, series_books, thumbnail):
|
def update_series_thumbnail(self, series_books, thumbnail):
|
||||||
thumbnail.generated_at = datetime.utcnow()
|
thumbnail.generated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ def render_task_status(tasklist):
|
|||||||
ret['task_id'] = task.id
|
ret['task_id'] = task.id
|
||||||
ret['stat'] = task.stat
|
ret['stat'] = task.stat
|
||||||
ret['is_cancellable'] = task.is_cancellable
|
ret['is_cancellable'] = task.is_cancellable
|
||||||
|
ret['error'] = task.error
|
||||||
|
|
||||||
rendered_tasklist.append(ret)
|
rendered_tasklist.append(ret)
|
||||||
|
|
||||||
|
|||||||
@@ -62,11 +62,9 @@
|
|||||||
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for format in entry.Books.data %}
|
{% if entry.Books.data|music %}
|
||||||
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
|
|
||||||
<span class="glyphicon glyphicon-music"></span>
|
<span class="glyphicon glyphicon-music"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
{% if entry.Books.series.__len__() > 0 %}
|
{% if entry.Books.series.__len__() > 0 %}
|
||||||
<p class="series">
|
<p class="series">
|
||||||
|
|||||||
@@ -47,26 +47,41 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.role_upload() and g.allow_upload %}
|
||||||
|
|
||||||
|
<div class="text-center more-stuff"><!--h4 aria-label="Upload new book format"></h4-->
|
||||||
|
<form id="form-upload-format" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="text-center">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="book_id" value="{{ book.id }}">
|
||||||
|
<div role="group" aria-label="Upload new book format">
|
||||||
|
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
|
||||||
|
<div class="upload-format-input-text" id="upload-format"></div>
|
||||||
|
<input id="btn-upload-format" name="btn-upload-format" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<form role="form" action="{{ url_for('edit-book.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
|
<form role="form" action="{{ url_for('edit-book.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="col-sm-9 col-xs-12">
|
<div class="col-sm-9 col-xs-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="book_title">{{_('Book Title')}}</label>
|
<label for="title">{{_('Book Title')}}</label>
|
||||||
<input type="text" class="form-control" name="book_title" id="book_title" value="{{book.title}}">
|
<input type="text" class="form-control" name="title" id="title" value="{{book.title}}">
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button>
|
<button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="author_div" class="form-group">
|
<div id="author_div" class="form-group">
|
||||||
<label for="bookAuthor">{{_('Author')}}</label>
|
<label for="bookAuthor">{{_('Author')}}</label>
|
||||||
<input type="text" class="form-control typeahead" autocomplete="off" name="author_name" id="bookAuthor" value="{{' & '.join(authors)}}">
|
<input type="text" class="form-control typeahead" autocomplete="off" name="authors" id="authors" value="{{' & '.join(authors)}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">{{_('Description')}}</label>
|
<label for="comments">{{_('Description')}}</label>
|
||||||
<textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
|
<textarea class="form-control" name="comments" id="comments" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -196,13 +211,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.role_upload() and g.allow_upload %}
|
|
||||||
<div role="group" aria-label="Upload new book format">
|
|
||||||
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
|
|
||||||
<div class="upload-format-input-text" id="upload-format"></div>
|
|
||||||
<input id="btn-upload-format" name="btn-upload-format" type="file">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
@@ -288,7 +296,7 @@
|
|||||||
'no_result': {{_('No Result(s) found! Please try another keyword.')|safe|tojson}},
|
'no_result': {{_('No Result(s) found! Please try another keyword.')|safe|tojson}},
|
||||||
'author': {{_('Author')|safe|tojson}},
|
'author': {{_('Author')|safe|tojson}},
|
||||||
'publisher': {{_('Publisher')|safe|tojson}},
|
'publisher': {{_('Publisher')|safe|tojson}},
|
||||||
'description': {{_('Description')|safe|tojson}},
|
'comments': {{_('Description')|safe|tojson}},
|
||||||
'source': {{_('Source')|safe|tojson}},
|
'source': {{_('Source')|safe|tojson}},
|
||||||
};
|
};
|
||||||
var language = '{{ current_user.locale }}';
|
var language = '{{ current_user.locale }}';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{{ _('Download') }} :
|
{{ _('Download') }} :
|
||||||
</button>
|
</button>
|
||||||
{% for format in entry.data %}
|
{% for format in entry.data %}
|
||||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}"
|
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower|replace('kepub', 'kepub.epub')) }}"
|
||||||
id="btnGroupDrop1{{ format.format|lower }}" class="btn btn-primary"
|
id="btnGroupDrop1{{ format.format|lower }}" class="btn btn-primary"
|
||||||
role="button">
|
role="button">
|
||||||
<span class="glyphicon glyphicon-download"></span>{{ format.format }}
|
<span class="glyphicon glyphicon-download"></span>{{ format.format }}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||||
{% for format in entry.data %}
|
{% for format in entry.data %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{ format.format }}
|
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower|replace('kepub', 'kepub.epub')) }}">{{ format.format }}
|
||||||
({{ format.uncompressed_size|filesizeformat }})</a></li>
|
({{ format.uncompressed_size|filesizeformat }})</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -15,6 +15,6 @@
|
|||||||
<img
|
<img
|
||||||
srcset="{{ srcset }}"
|
srcset="{{ srcset }}"
|
||||||
src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='day'|cache_timestamp) }}"
|
src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='day'|cache_timestamp) }}"
|
||||||
alt="{{ book_title }}"
|
alt="{{ title }}"
|
||||||
/>
|
/>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -119,11 +119,9 @@
|
|||||||
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='stored') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='stored') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for format in entry.Books.data %}
|
{% if entry.Books.data|music %}
|
||||||
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
|
|
||||||
<span class="glyphicon glyphicon-music"></span>
|
<span class="glyphicon glyphicon-music"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%endfor%}
|
|
||||||
</p>
|
</p>
|
||||||
{% if entry.Books.series.__len__() > 0 %}
|
{% if entry.Books.series.__len__() > 0 %}
|
||||||
<p class="series">
|
<p class="series">
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
|
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
|
||||||
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
|
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
|
||||||
|
<input class="hide" id="btn-upload2" name="btn-upload2" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"input": {
|
|
||||||
"placeholder": "a placeholder"
|
|
||||||
},
|
|
||||||
"nav": {
|
|
||||||
"home": "Home",
|
|
||||||
"page1": "Page One",
|
|
||||||
"page2": "Page Two"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -73,11 +73,9 @@
|
|||||||
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for format in entry.Books.data %}
|
{% if entry.Books.data|music %}
|
||||||
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
|
|
||||||
<span class="glyphicon glyphicon-music"></span>
|
<span class="glyphicon glyphicon-music"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
</p>
|
</p>
|
||||||
{% if entry.Books.series.__len__() > 0 %}
|
{% if entry.Books.series.__len__() > 0 %}
|
||||||
<p class="series">
|
<p class="series">
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<form role="form" id="search" action="{{ url_for('search.advanced_search_form') }}" method="POST">
|
<form role="form" id="search" action="{{ url_for('search.advanced_search_form') }}" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="book_title">{{_('Book Title')}}</label>
|
<label for="title">{{_('Book Title')}}</label>
|
||||||
<input type="text" class="form-control" name="book_title" id="book_title" value="">
|
<input type="text" class="form-control" name="title" id="title" value="">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bookAuthor">{{_('Author')}}</label>
|
<label for="authors">{{_('Author')}}</label>
|
||||||
<input type="text" class="form-control typeahead" name="author_name" id="bookAuthor" value="" autocomplete="off">
|
<input type="text" class="form-control typeahead" name="authors" id="authors" value="" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="Publisher">{{_('Publisher')}}</label>
|
<label for="Publisher">{{_('Publisher')}}</label>
|
||||||
@@ -151,28 +151,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="comment">{{_('Description')}}</label>
|
<label for="comments">{{_('Description')}}</label>
|
||||||
<input type="text" class="form-control" name="comment" id="comment" value="">
|
<input type="text" class="form-control" name="comments" id="comments" value="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if cc|length > 0 %}
|
{% if cc|length > 0 %}
|
||||||
{% for c in cc %}
|
{% for c in cc %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<!--input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""-->
|
||||||
<label for="{{ 'custom_column_' ~ c.id }}">{{ c.name }}</label>
|
<label for="{{ 'custom_column_' ~ c.id }}">{{ c.name }}</label>
|
||||||
{% if c.datatype == 'bool' %}
|
{% if c.datatype == 'bool' %}
|
||||||
<select name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" class="form-control">
|
<select name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" class="form-control">
|
||||||
<option value="" selected></option>
|
<option value="Any" selected>{{_('Any')}}</option>
|
||||||
|
<option value="">{{_('Empty')}}</option>
|
||||||
<option value="True" >{{_('Yes')}}</option>
|
<option value="True" >{{_('Yes')}}</option>
|
||||||
<option value="False" >{{_('No')}}</option>
|
<option value="False" >{{_('No')}}</option>
|
||||||
</select>
|
</select>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if c.datatype == 'int' %}
|
{% if c.datatype == 'int' %}
|
||||||
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
|
<div class="row">
|
||||||
|
<div class="form-group col-sm-6">
|
||||||
|
<label for="{{ 'custom_column_' ~ c.id }}_low">{{_('From:')}}</label>
|
||||||
|
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}_low" id="{{ 'custom_column_' ~ c.id }}_low" value="">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-sm-6">
|
||||||
|
<label for="{{ 'custom_column_' ~ c.id }}_high">{{_('To:')}}</label>
|
||||||
|
<input type="number" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}_high" id="{{ 'custom_column_' ~ c.id }}_high" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if c.datatype == 'float' %}
|
{% if c.datatype == 'float' %}
|
||||||
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="">
|
<div class="row">
|
||||||
|
<div class="form-group col-sm-6">
|
||||||
|
<label for="{{ 'custom_column_' ~ c.id }}_low">{{_('From:')}}</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}_low" id="{{ 'custom_column_' ~ c.id }}_low" value="">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-sm-6">
|
||||||
|
<label for="{{ 'custom_column_' ~ c.id }}_high">{{_('To:')}}</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}_high" id="{{ 'custom_column_' ~ c.id }}_high" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--input type="number" step="0.01" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value=""-->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if c.datatype == 'datetime' %}
|
{% if c.datatype == 'datetime' %}
|
||||||
|
|||||||
@@ -15,16 +15,19 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if entries.__len__() %}
|
{% if entries.__len__() %}
|
||||||
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
|
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
|
||||||
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
|
<button id="toggle_order_shelf" data-alt-text="{% if status == "on" %}{{_('Disable Change order')}}{% else %}{{_('Enable Change order')}}{% endif %}" data-view="shelf" class="btn btn-primary{% if status == 'on' %} dummy">{{ _('Enable Change order') }}{% else%}">{{ _('Disable Change order') }}{% endif %}</button>
|
||||||
<div class="filterheader hidden-xs">
|
<div class="filterheader hidden-xs">
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary {% if order == "new" %} active{% endif %}{% if status == "on" %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary {% if order == 'old' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary {% if order == 'abc' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary {% if order == 'zyx' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary {% if order == 'authaz' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary {% if order == 'authza' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary {% if order == 'pubnew' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary {% if order == 'pubold' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to book added to shelf, newest first')}}" id="shelf_new" class="btn btn-primary {% if order == 'shelfnew' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='shelfnew')}}"><span class="glyphicon glyphicon-list"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to book added to shelf, oldest first')}}" id="shelf_old" class="btn btn-primary {% if order == 'shelfold' %} active{% endif %}{% if status == 'on' %} disabled{% endif %}" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='shelfold')}}"><span class="glyphicon glyphicon-list"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
{% if entry.Books.data|length %}
|
{% if entry.Books.data|length %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% for format in entry.Books.data %}
|
{% for format in entry.Books.data %}
|
||||||
<a href="{{ url_for('web.download_link', book_id=entry.Books.id, book_format=format.format|lower, anyname=entry.Books.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.Books.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
|
<a href="{{ url_for('web.download_link', book_id=entry.Books.id, book_format=format.format|lower, anyname=entry.Books.id|string+'.'+format.format|lower|replace('kepub', 'kepub.epub')) }}" id="btnGroupDrop{{entry.Books.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
|
||||||
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
|
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
|
||||||
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th>
|
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th>
|
||||||
<th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th>
|
<th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th>
|
||||||
|
<th data-halign="right" data-align="right" data-field="error" data-sortable="true">{{_('Message')}}</th>
|
||||||
{% if current_user.role_admin() %}
|
{% if current_user.role_admin() %}
|
||||||
<th data-halign="right" data-align="right" data-formatter="TaskActions" data-switchable="false">{{_('Actions')}}</th>
|
<th data-halign="right" data-align="right" data-formatter="TaskActions" data-switchable="false">{{_('Actions')}}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="kindle_mail">{{_('Send to eReader Email Address. Use comma to seperate emails for multiple eReaders')}}</label>
|
<label for="kindle_mail">{{_('Send to eReader Email Address. Use comma to separate emails for multiple eReaders')}}</label>
|
||||||
<input type="email" class="form-control" name="kindle_mail" id="kindle_mail" value="{{ content.kindle_mail if content.kindle_mail != None }}">
|
<input type="email" class="form-control" name="kindle_mail" id="kindle_mail" value="{{ content.kindle_mail if content.kindle_mail != None }}">
|
||||||
</div>
|
</div>
|
||||||
{% if not content.role_anonymous() %}
|
{% if not content.role_anonymous() %}
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user