mirror of
https://github.com/janeczku/calibre-web
synced 2024-09-28 23:10:48 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
1f0b569cf7
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,4 +1,5 @@
|
|||||||
constants.py ident export-subst
|
constants.py ident export-subst
|
||||||
/test export-ignore
|
/test export-ignore
|
||||||
|
/library export-ignore
|
||||||
cps/static/css/libs/* linguist-vendored
|
cps/static/css/libs/* linguist-vendored
|
||||||
cps/static/js/libs/* linguist-vendored
|
cps/static/js/libs/* linguist-vendored
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -28,8 +28,10 @@ cps/cache
|
|||||||
.idea/
|
.idea/
|
||||||
*.bak
|
*.bak
|
||||||
*.log.*
|
*.log.*
|
||||||
|
.key
|
||||||
|
|
||||||
settings.yaml
|
settings.yaml
|
||||||
gdrive_credentials
|
gdrive_credentials
|
||||||
client_secrets.json
|
client_secrets.json
|
||||||
gmail.json
|
gmail.json
|
||||||
|
/.key
|
||||||
|
139
README.md
139
README.md
@ -1,99 +1,118 @@
|
|||||||
# About
|
# Calibre-Web
|
||||||
|
|
||||||
Calibre-Web is a web app providing a clean 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.
|
||||||
|
|
||||||
[![GitHub License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
[![License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
|
||||||
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)]()
|
![Commit Activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)
|
||||||
[![GitHub all releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
|
[![All Releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
|
||||||
[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
||||||
[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
|
||||||
[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB)
|
[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Table of Contents</strong> (click to expand)</summary>
|
||||||
|
|
||||||
|
1. [About](#calibre-web)
|
||||||
|
2. [Features](#features)
|
||||||
|
3. [Installation](#installation)
|
||||||
|
- [Installation via pip (recommended)](#installation-via-pip-recommended)
|
||||||
|
- [Quick start](#quick-start)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
4. [Docker Images](#docker-images)
|
||||||
|
5. [Contributor Recognition](#contributor-recognition)
|
||||||
|
6. [Contact](#contact)
|
||||||
|
7. [Contributing to Calibre-Web](#contributing-to-calibre-web)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
||||||
|
|
||||||
![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png)
|
![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Bootstrap 3 HTML5 interface
|
- Modern and responsive Bootstrap 3 HTML5 interface
|
||||||
- full graphical setup
|
- Full graphical setup
|
||||||
- User management with fine-grained per-user permissions
|
- Comprehensive user management with fine-grained per-user permissions
|
||||||
- Admin interface
|
- Admin interface
|
||||||
- User Interface in brazilian, czech, dutch, english, finnish, french, galician, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian, vietnamese
|
- Multilingual user interface supporting 20+ languages ([supported languages](https://github.com/janeczku/calibre-web/wiki/Translation-Status))
|
||||||
- OPDS feed for eBook reader apps
|
- OPDS feed for eBook reader apps
|
||||||
- Filter and search by titles, authors, tags, series, book format and language
|
- Advanced search and filtering options
|
||||||
- Create a custom book collection (shelves)
|
- Custom book collection (shelves) creation
|
||||||
- Support for editing eBook metadata and deleting eBooks from Calibre library
|
- eBook metadata editing and deletion support
|
||||||
- Support for downloading eBook metadata from various sources, sources can be extended via external plugins
|
- Metadata download from various sources (extensible via plugins)
|
||||||
- Support for converting eBooks through Calibre binaries
|
- eBook conversion through Calibre binaries
|
||||||
- Restrict eBook download to logged-in users
|
- eBook download restriction to logged-in users
|
||||||
- Support for public user registration
|
- Public user registration support
|
||||||
- Send eBooks to E-Readers with the click of a button
|
- Send eBooks to E-Readers with a single click
|
||||||
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
- Sync Kobo devices with your Calibre library
|
||||||
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
|
- In-browser eBook reading support for multiple formats
|
||||||
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
- Upload new books in various formats, including audio formats
|
||||||
- Support for Calibre Custom Columns
|
- Calibre Custom Columns support
|
||||||
- Ability to hide content based on categories and Custom Column content per user
|
- Content hiding based on categories and Custom Column content per user
|
||||||
- Self-update capability
|
- Self-update capability
|
||||||
- "Magic Link" login to make it easy to log on eReaders
|
- "Magic Link" login for easy access on eReaders
|
||||||
- Login via LDAP, google/github oauth and via proxy authentication
|
- LDAP, Google/GitHub OAuth, and proxy authentication support
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
#### Installation via pip (recommended)
|
#### Installation via pip (recommended)
|
||||||
1. To avoid problems with already installed python dependencies, it's recommended to create a virtual environment for Calibre-Web
|
1. Create a virtual environment for Calibre-Web to avoid conflicts with existing Python dependencies
|
||||||
2. Install Calibre-Web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
|
2. Install Calibre-Web via pip: `pip install calibreweb` (or `pip3` depending on your OS/distro)
|
||||||
3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
|
3. Install optional features via pip as needed, see [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
|
||||||
4. Calibre-Web can be started afterwards by typing `cps`
|
4. Start Calibre-Web by typing `cps`
|
||||||
|
|
||||||
In the Wiki there are also examples for: a [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [installation on Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:Install-Calibre-Web-in-Linux-Mint-19-or-20), [installation on a Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
|
*Note: Raspberry Pi OS users may encounter issues during installation. If so, please update pip (`./venv/bin/python3 -m pip install --upgrade pip`) and/or install cargo (`sudo apt install cargo`) before retrying the installation.*
|
||||||
|
|
||||||
## Quick start
|
Refer to the Wiki for additional installation examples: [manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation), [Linux Mint](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-in-Linux-Mint-19-or-20), [Cloud Provider](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider).
|
||||||
|
|
||||||
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
|
## Quick Start
|
||||||
Login with default admin login \
|
|
||||||
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button \
|
|
||||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration) \
|
|
||||||
Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page)
|
|
||||||
|
|
||||||
#### Default admin login:
|
1. Open your browser and navigate to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||||
*Username:* admin\
|
2. Log in with the default admin credentials
|
||||||
*Password:* admin123
|
3. If you don't have a Calibre database, you can use [this database](https://github.com/janeczku/calibre-web/raw/master/library/metadata.db) (move it out of the Calibre-Web folder to prevent overwriting during updates)
|
||||||
|
4. Set `Location of Calibre database` to the path of the folder containing your Calibre library (metadata.db) and click "Save"
|
||||||
|
5. Optionally, use Google Drive to host your Calibre library by following the [Google Drive integration guide](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration)
|
||||||
|
6. Configure your Calibre-Web instance via the admin page, referring to the [Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) guides
|
||||||
|
|
||||||
|
#### Default Admin Login:
|
||||||
|
- **Username:** admin
|
||||||
|
- **Password:** admin123
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
python 3.5+
|
- Python 3.5+
|
||||||
|
- [Imagemagick](https://imagemagick.org/script/download.php) for cover extraction from EPUBs (Windows users may need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction)
|
||||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-ereader feature, or during editing of ebooks metadata:
|
- Optional: [Calibre desktop program](https://calibre-ebook.com/download) for on-the-fly conversion and metadata editing (set "calibre's converter tool" path on the setup page)
|
||||||
|
- Optional: [Kepubify tool](https://github.com/pgaskin/kepubify/releases/latest) for Kobo device support (place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows)
|
||||||
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
|
|
||||||
|
|
||||||
[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `/opt/kepubify` Windows: `C:\Program Files\kepubify`.
|
|
||||||
|
|
||||||
## Docker Images
|
## Docker Images
|
||||||
|
|
||||||
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
Pre-built Docker images are available in the following Docker Hub repositories (maintained by the LinuxServer team):
|
||||||
|
|
||||||
#### **LinuxServer - x64, armhf, aarch64**
|
#### **LinuxServer - x64, aarch64**
|
||||||
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
- [Docker Hub](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||||
+ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
|
- [GitHub](https://github.com/linuxserver/docker-calibre-web)
|
||||||
+ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre)
|
- [GitHub - Optional Calibre layer](https://github.com/linuxserver/docker-mods/tree/universal-calibre)
|
||||||
|
|
||||||
This image has the option to pull in an extra docker manifest layer to include the Calibre `ebook-convert` binary. Just include the environmental variable `DOCKER_MODS=linuxserver/calibre-web:calibre` in your docker run/docker compose file. **(x64 only)**
|
Include the environment variable `DOCKER_MODS=linuxserver/mods:universal-calibre` in your Docker run/compose file to add the Calibre `ebook-convert` binary (x64 only). Omit this variable for a lightweight image.
|
||||||
|
|
||||||
If you do not need this functionality then this can be omitted, keeping the image as lightweight as possible.
|
Both the Calibre-Web and Calibre-Mod images are automatically rebuilt on new releases and updates.
|
||||||
|
|
||||||
Both the Calibre-Web and Calibre-Mod images are rebuilt automatically on new releases of Calibre-Web and Calibre respectively, and on updates to any included base image packages on a weekly basis if required.
|
- Set "path to convertertool" to `/usr/bin/ebook-convert`
|
||||||
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
- Set "path to unrar" to `/usr/bin/unrar`
|
||||||
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
|
||||||
|
|
||||||
# Contact
|
## Contributor Recognition
|
||||||
|
|
||||||
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
|
We would like to thank all the [contributors](https://github.com/janeczku/calibre-web/graphs/contributors) and maintainers of Calibre-Web for their valuable input and dedication to the project. Your contributions are greatly appreciated.
|
||||||
|
|
||||||
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
## Contact
|
||||||
|
|
||||||
# Contributing to Calibre-Web
|
Join us on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
For more information, How To's, and FAQs, please visit the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
||||||
|
|
||||||
|
## Contributing to Calibre-Web
|
||||||
|
|
||||||
|
Check out our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
||||||
|
@ -38,6 +38,13 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
|
|||||||
| 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 | Login is rate limited ||
|
||||||
|
| 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 | 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 | Error Handling at non existent rating, language, and user downloaded books was fixed ||
|
||||||
|
|
||||||
|
|
||||||
## Statement regarding Log4j (CVE-2021-44228 and related)
|
## Statement regarding Log4j (CVE-2021-44228 and related)
|
||||||
|
@ -2,4 +2,3 @@
|
|||||||
|
|
||||||
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
|
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
|
||||||
[jinja2: **/templates/**.*ml]
|
[jinja2: **/templates/**.*ml]
|
||||||
extensions=jinja2.ext.with_
|
|
||||||
|
2
cps.py
2
cps.py
@ -21,7 +21,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# Add local path to sys.path so we can import cps
|
# Add local path to sys.path, so we can import cps
|
||||||
path = os.path.dirname(os.path.abspath(__file__))
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, path)
|
sys.path.insert(0, path)
|
||||||
|
|
||||||
|
@ -21,15 +21,32 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager, confirm_login
|
||||||
from flask import session
|
from flask import session, current_app
|
||||||
|
from flask_login.utils import decode_cookie
|
||||||
|
from flask_login.signals import user_loaded_from_cookie
|
||||||
|
|
||||||
class MyLoginManager(LoginManager):
|
class MyLoginManager(LoginManager):
|
||||||
def _session_protection_failed(self):
|
def _session_protection_failed(self):
|
||||||
_session = session._get_current_object()
|
sess = session._get_current_object()
|
||||||
ident = self._session_identifier_generator()
|
ident = self._session_identifier_generator()
|
||||||
if(_session and not (len(_session) == 1
|
if(sess and not (len(sess) == 1
|
||||||
and _session.get('csrf_token', None))) and ident != _session.get('_id', None):
|
and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
|
||||||
return super(). _session_protection_failed()
|
return super(). _session_protection_failed()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _load_user_from_remember_cookie(self, cookie):
|
||||||
|
user_id = decode_cookie(cookie)
|
||||||
|
if user_id is not None:
|
||||||
|
session["_user_id"] = user_id
|
||||||
|
session["_fresh"] = False
|
||||||
|
user = None
|
||||||
|
if self._user_callback:
|
||||||
|
user = self._user_callback(user_id)
|
||||||
|
if user is not None:
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
user_loaded_from_cookie.send(app, user=user)
|
||||||
|
# if session was restored from remember me cookie make login valid
|
||||||
|
confirm_login()
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
@ -36,11 +36,16 @@ from .reverseproxy import ReverseProxied
|
|||||||
from .server import WebServer
|
from .server import WebServer
|
||||||
from .dep_check import dependency_check
|
from .dep_check import dependency_check
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
from .babel import babel
|
from .babel import babel, get_locale
|
||||||
from . import config_sql
|
from . import config_sql
|
||||||
from . import cache_buster
|
from . import cache_buster
|
||||||
from . import ub, db
|
from . import ub, db
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_limiter import Limiter
|
||||||
|
limiter_present = True
|
||||||
|
except ImportError:
|
||||||
|
limiter_present = False
|
||||||
try:
|
try:
|
||||||
from flask_wtf.csrf import CSRFProtect
|
from flask_wtf.csrf import CSRFProtect
|
||||||
wtf_present = True
|
wtf_present = True
|
||||||
@ -59,7 +64,8 @@ mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
|
|||||||
mimetypes.add_type('application/x-cbr', '.cbr')
|
mimetypes.add_type('application/x-cbr', '.cbr')
|
||||||
mimetypes.add_type('application/x-cbz', '.cbz')
|
mimetypes.add_type('application/x-cbz', '.cbz')
|
||||||
mimetypes.add_type('application/x-cbt', '.cbt')
|
mimetypes.add_type('application/x-cbt', '.cbt')
|
||||||
mimetypes.add_type('image/vnd.djvu', '.djvu')
|
mimetypes.add_type('application/x-cb7', '.cb7')
|
||||||
|
mimetypes.add_type('image/vnd.djv', '.djv')
|
||||||
mimetypes.add_type('application/mpeg', '.mpeg')
|
mimetypes.add_type('application/mpeg', '.mpeg')
|
||||||
mimetypes.add_type('application/mpeg', '.mp3')
|
mimetypes.add_type('application/mpeg', '.mp3')
|
||||||
mimetypes.add_type('application/mp4', '.m4a')
|
mimetypes.add_type('application/mp4', '.m4a')
|
||||||
@ -81,10 +87,10 @@ app.config.update(
|
|||||||
|
|
||||||
lm = MyLoginManager()
|
lm = MyLoginManager()
|
||||||
|
|
||||||
config = config_sql._ConfigSQL()
|
|
||||||
|
|
||||||
cli_param = CliParameter()
|
cli_param = CliParameter()
|
||||||
|
|
||||||
|
config = config_sql.ConfigSQL()
|
||||||
|
|
||||||
if wtf_present:
|
if wtf_present:
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
else:
|
else:
|
||||||
@ -96,33 +102,36 @@ web_server = WebServer()
|
|||||||
|
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
|
||||||
|
if limiter_present:
|
||||||
|
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
|
||||||
|
else:
|
||||||
|
limiter = None
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
lm.login_view = 'web.login'
|
|
||||||
lm.anonymous_user = ub.Anonymous
|
|
||||||
lm.session_protection = 'strong'
|
|
||||||
|
|
||||||
if csrf:
|
if csrf:
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
|
|
||||||
cli_param.init()
|
cli_param.init()
|
||||||
|
|
||||||
ub.init_db(cli_param.settings_path, cli_param.user_credentials)
|
ub.init_db(cli_param.settings_path)
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
config_sql.load_configuration(config, ub.session, cli_param)
|
encrypt_key, error = config_sql.get_encryption_key(os.path.dirname(cli_param.settings_path))
|
||||||
|
|
||||||
db.CalibreDB.update_config(config)
|
config_sql.load_configuration(ub.session, encrypt_key)
|
||||||
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
|
config.init_config(ub.session, encrypt_key, cli_param)
|
||||||
calibre_db.init_db()
|
|
||||||
|
|
||||||
updater_thread.init_updater(config, web_server)
|
if error:
|
||||||
# Perform dry run of updater and exit afterwards
|
log.error(error)
|
||||||
if cli_param.dry_run:
|
|
||||||
updater_thread.dry_run()
|
|
||||||
sys.exit(0)
|
|
||||||
updater_thread.start()
|
|
||||||
|
|
||||||
|
ub.password_change(cli_param.user_credentials)
|
||||||
|
|
||||||
|
if not limiter:
|
||||||
|
log.info('*** "flask-limiter" is needed for calibre-web to run. '
|
||||||
|
'Please install it using pip: "pip install flask-limiter" ***')
|
||||||
|
print('*** "flask-limiter" is needed for calibre-web to run. '
|
||||||
|
'Please install it using pip: "pip install flask-limiter" ***')
|
||||||
|
web_server.stop(True)
|
||||||
|
sys.exit(8)
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
log.info(
|
log.info(
|
||||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
|
||||||
@ -139,8 +148,24 @@ def create_app():
|
|||||||
'Please install it using pip: "pip install flask-WTF" ***')
|
'Please install it using pip: "pip install flask-WTF" ***')
|
||||||
web_server.stop(True)
|
web_server.stop(True)
|
||||||
sys.exit(7)
|
sys.exit(7)
|
||||||
|
|
||||||
|
lm.login_view = 'web.login'
|
||||||
|
lm.anonymous_user = ub.Anonymous
|
||||||
|
lm.session_protection = 'strong' if config.config_session == 1 else "basic"
|
||||||
|
|
||||||
|
db.CalibreDB.update_config(config)
|
||||||
|
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
|
||||||
|
calibre_db.init_db()
|
||||||
|
|
||||||
|
updater_thread.init_updater(config, web_server)
|
||||||
|
# Perform dry run of updater and exit afterwards
|
||||||
|
if cli_param.dry_run:
|
||||||
|
updater_thread.dry_run()
|
||||||
|
sys.exit(0)
|
||||||
|
updater_thread.start()
|
||||||
|
|
||||||
for res in dependency_check() + dependency_check(True):
|
for res in dependency_check() + dependency_check(True):
|
||||||
log.info('*** "{}" version does not fit the requirements. '
|
log.info('*** "{}" version does not meet the requirements. '
|
||||||
'Should: {}, Found: {}, please consider installing required version ***'
|
'Should: {}, Found: {}, please consider installing required version ***'
|
||||||
.format(res['name'],
|
.format(res['name'],
|
||||||
res['target'],
|
res['target'],
|
||||||
@ -150,14 +175,16 @@ def create_app():
|
|||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
cache_buster.init_cache_busting(app)
|
cache_buster.init_cache_busting(app)
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
|
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||||
|
|
||||||
web_server.init_app(app, config)
|
web_server.init_app(app, config)
|
||||||
|
if hasattr(babel, "localeselector"):
|
||||||
babel.init_app(app)
|
babel.init_app(app)
|
||||||
|
babel.localeselector(get_locale)
|
||||||
|
else:
|
||||||
|
babel.init_app(app, locale_selector=get_locale)
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
@ -165,9 +192,13 @@ def create_app():
|
|||||||
services.ldap.init_app(app, config)
|
services.ldap.init_app(app, config)
|
||||||
if services.goodreads_support:
|
if services.goodreads_support:
|
||||||
services.goodreads_support.connect(config.config_goodreads_api_key,
|
services.goodreads_support.connect(config.config_goodreads_api_key,
|
||||||
config.config_goodreads_api_secret,
|
config.config_goodreads_api_secret_e,
|
||||||
config.config_use_goodreads)
|
config.config_use_goodreads)
|
||||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
||||||
|
# Configure rate limiter
|
||||||
|
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
|
||||||
|
limiter.init_app(app)
|
||||||
|
|
||||||
# Register scheduled tasks
|
# Register scheduled tasks
|
||||||
from .schedule import register_scheduled_tasks, register_startup_tasks
|
from .schedule import register_scheduled_tasks, register_startup_tasks
|
||||||
register_scheduled_tasks(config.schedule_reconnect)
|
register_scheduled_tasks(config.schedule_reconnect)
|
||||||
|
@ -81,4 +81,4 @@ def stats():
|
|||||||
categories = calibre_db.session.query(db.Tags).count()
|
categories = calibre_db.session.query(db.Tags).count()
|
||||||
series = calibre_db.session.query(db.Series).count()
|
series = calibre_db.session.query(db.Series).count()
|
||||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
||||||
categorycounter=categories, seriecounter=series, title=_(u"Statistics"), page="stat")
|
categorycounter=categories, seriecounter=series, title=_("Statistics"), page="stat")
|
||||||
|
198
cps/admin.py
198
cps/admin.py
@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
import time
|
import time
|
||||||
@ -31,9 +30,11 @@ import string
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from datetime import time as datetime_time
|
from datetime import time as datetime_time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
from markupsafe import Markup
|
||||||
|
from flask_login import login_required, current_user, logout_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
@ -101,25 +102,26 @@ def admin_required(f):
|
|||||||
|
|
||||||
@admi.before_app_request
|
@admi.before_app_request
|
||||||
def before_request():
|
def before_request():
|
||||||
# make remember me function work
|
try:
|
||||||
if current_user.is_authenticated:
|
if not ub.check_user_session(current_user.id,
|
||||||
confirm_login()
|
flask_session.get('_id')) and 'opds' not in request.path \
|
||||||
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
|
and config.config_session == 1:
|
||||||
logout_user()
|
logout_user()
|
||||||
|
except AttributeError:
|
||||||
|
pass # ? fails on requesting /ajax/emailstat during restart ?
|
||||||
g.constants = constants
|
g.constants = constants
|
||||||
g.user = current_user
|
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION', '')
|
||||||
g.allow_registration = config.config_public_reg
|
g.allow_registration = config.config_public_reg
|
||||||
g.allow_anonymous = config.config_anonbrowse
|
g.allow_anonymous = config.config_anonbrowse
|
||||||
g.allow_upload = config.config_uploading
|
g.allow_upload = config.config_uploading
|
||||||
g.current_theme = config.config_theme
|
g.current_theme = config.config_theme
|
||||||
g.config_authors_max = config.config_authors_max
|
g.config_authors_max = config.config_authors_max
|
||||||
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
|
||||||
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
|
||||||
if '/static/' not in request.path and not config.db_configured and \
|
if '/static/' not in request.path and not config.db_configured and \
|
||||||
request.endpoint not in ('admin.ajax_db_config',
|
request.endpoint not in ('admin.ajax_db_config',
|
||||||
'admin.simulatedbchange',
|
'admin.simulatedbchange',
|
||||||
'admin.db_configuration',
|
'admin.db_configuration',
|
||||||
'web.login',
|
'web.login',
|
||||||
|
'web.login_post',
|
||||||
'web.logout',
|
'web.logout',
|
||||||
'admin.load_dialogtexts',
|
'admin.load_dialogtexts',
|
||||||
'admin.ajax_pathchooser'):
|
'admin.ajax_pathchooser'):
|
||||||
@ -144,9 +146,9 @@ def shutdown():
|
|||||||
ub.dispose()
|
ub.dispose()
|
||||||
|
|
||||||
if task == 0:
|
if task == 0:
|
||||||
show_text['text'] = _(u'Server restarted, please reload page')
|
show_text['text'] = _('Server restarted, please reload page.')
|
||||||
else:
|
else:
|
||||||
show_text['text'] = _(u'Performing shutdown of server, please close window')
|
show_text['text'] = _('Performing Server shutdown, please close window.')
|
||||||
# stop gevent/tornado server
|
# stop gevent/tornado server
|
||||||
web_server.stop(task == 0)
|
web_server.stop(task == 0)
|
||||||
return json.dumps(show_text)
|
return json.dumps(show_text)
|
||||||
@ -154,10 +156,10 @@ def shutdown():
|
|||||||
if task == 2:
|
if task == 2:
|
||||||
log.warning("reconnecting to calibre database")
|
log.warning("reconnecting to calibre database")
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
show_text['text'] = _(u'Reconnect successful')
|
show_text['text'] = _('Success! Database Reconnected')
|
||||||
return json.dumps(show_text)
|
return json.dumps(show_text)
|
||||||
|
|
||||||
show_text['text'] = _(u'Unknown command')
|
show_text['text'] = _('Unknown command')
|
||||||
return json.dumps(show_text), 400
|
return json.dumps(show_text), 400
|
||||||
|
|
||||||
|
|
||||||
@ -168,7 +170,7 @@ def queue_metadata_backup():
|
|||||||
show_text = {}
|
show_text = {}
|
||||||
log.warning("Queuing all books for metadata backup")
|
log.warning("Queuing all books for metadata backup")
|
||||||
helper.set_all_metadata_dirty()
|
helper.set_all_metadata_dirty()
|
||||||
show_text['text'] = _(u'Books successfully queued for Metadata Backup')
|
show_text['text'] = _('Success! Books queued for Metadata Backup, please check Tasks for result')
|
||||||
return json.dumps(show_text)
|
return json.dumps(show_text)
|
||||||
|
|
||||||
|
|
||||||
@ -201,7 +203,7 @@ def update_thumbnails():
|
|||||||
def admin():
|
def admin():
|
||||||
version = updater_thread.get_current_version_info()
|
version = updater_thread.get_current_version_info()
|
||||||
if version is False:
|
if version is False:
|
||||||
commit = _(u'Unknown')
|
commit = _('Unknown')
|
||||||
else:
|
else:
|
||||||
if 'datetime' in version:
|
if 'datetime' in version:
|
||||||
commit = version['datetime']
|
commit = version['datetime']
|
||||||
@ -218,15 +220,15 @@ def admin():
|
|||||||
commit = version['version']
|
commit = version['version']
|
||||||
|
|
||||||
all_user = ub.session.query(ub.User).all()
|
all_user = ub.session.query(ub.User).all()
|
||||||
email_settings = config.get_mail_settings()
|
# email_settings = mail_config.get_mail_settings()
|
||||||
schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short")
|
schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short")
|
||||||
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
|
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
|
||||||
schedule_duration = format_timedelta(t, threshold=.99)
|
schedule_duration = format_timedelta(t, threshold=.99)
|
||||||
|
|
||||||
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
|
return render_title_template("admin.html", allUser=all_user, config=config, commit=commit,
|
||||||
feature_support=feature_support, schedule_time=schedule_time,
|
feature_support=feature_support, schedule_time=schedule_time,
|
||||||
schedule_duration=schedule_duration,
|
schedule_duration=schedule_duration,
|
||||||
title=_(u"Admin page"), page="admin")
|
title=_("Admin page"), page="admin")
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
|
||||||
@ -246,7 +248,7 @@ def configuration():
|
|||||||
config=config,
|
config=config,
|
||||||
provider=oauthblueprints,
|
provider=oauthblueprints,
|
||||||
feature_support=feature_support,
|
feature_support=feature_support,
|
||||||
title=_(u"Basic Configuration"), page="config")
|
title=_("Basic Configuration"), page="config")
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
@admi.route("/admin/ajaxconfig", methods=["POST"])
|
||||||
@ -284,7 +286,7 @@ def view_configuration():
|
|||||||
restrictColumns=restrict_columns,
|
restrictColumns=restrict_columns,
|
||||||
languages=languages,
|
languages=languages,
|
||||||
translations=translations,
|
translations=translations,
|
||||||
title=_(u"UI Configuration"), page="uiconfig")
|
title=_("UI Configuration"), page="uiconfig")
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/usertable")
|
@admi.route("/admin/usertable")
|
||||||
@ -318,7 +320,7 @@ def edit_user_table():
|
|||||||
all_roles=constants.ALL_ROLES,
|
all_roles=constants.ALL_ROLES,
|
||||||
kobo_support=kobo_support,
|
kobo_support=kobo_support,
|
||||||
sidebar_settings=constants.sidebar_settings,
|
sidebar_settings=constants.sidebar_settings,
|
||||||
title=_(u"Edit Users"),
|
title=_("Edit Users"),
|
||||||
page="usertable")
|
page="usertable")
|
||||||
|
|
||||||
|
|
||||||
@ -489,7 +491,7 @@ def edit_list_user(param):
|
|||||||
ub.User.id != user.id).count():
|
ub.User.id != user.id).count():
|
||||||
return Response(
|
return Response(
|
||||||
json.dumps([{'type': "danger",
|
json.dumps([{'type': "danger",
|
||||||
'message': _(u"No admin user remaining, can't remove admin role",
|
'message': _("No admin user remaining, can't remove admin role",
|
||||||
nick=user.name)}]), mimetype='application/json')
|
nick=user.name)}]), mimetype='application/json')
|
||||||
user.role &= ~value
|
user.role &= ~value
|
||||||
else:
|
else:
|
||||||
@ -566,13 +568,13 @@ def update_view_configuration():
|
|||||||
calibre_db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
|
|
||||||
if not check_valid_read_column(to_save.get("config_read_column", "0")):
|
if not check_valid_read_column(to_save.get("config_read_column", "0")):
|
||||||
flash(_(u"Invalid Read Column"), category="error")
|
flash(_("Invalid Read Column"), category="error")
|
||||||
log.debug("Invalid Read column")
|
log.debug("Invalid Read column")
|
||||||
return view_configuration()
|
return view_configuration()
|
||||||
_config_int(to_save, "config_read_column")
|
_config_int(to_save, "config_read_column")
|
||||||
|
|
||||||
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
|
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
|
||||||
flash(_(u"Invalid Restricted Column"), category="error")
|
flash(_("Invalid Restricted Column"), category="error")
|
||||||
log.debug("Invalid Restricted Column")
|
log.debug("Invalid Restricted Column")
|
||||||
return view_configuration()
|
return view_configuration()
|
||||||
_config_int(to_save, "config_restricted_column")
|
_config_int(to_save, "config_restricted_column")
|
||||||
@ -592,7 +594,7 @@ def update_view_configuration():
|
|||||||
config.config_default_show |= constants.DETAIL_RANDOM
|
config.config_default_show |= constants.DETAIL_RANDOM
|
||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
flash(_(u"Calibre-Web configuration updated"), category="success")
|
flash(_("Calibre-Web configuration updated"), category="success")
|
||||||
log.debug("Calibre-Web configuration updated")
|
log.debug("Calibre-Web configuration updated")
|
||||||
before_request()
|
before_request()
|
||||||
|
|
||||||
@ -1037,7 +1039,8 @@ def pathchooser():
|
|||||||
|
|
||||||
for f in folders:
|
for f in folders:
|
||||||
try:
|
try:
|
||||||
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
sanitized_f = str(Markup.escape(f))
|
||||||
|
data = {"name": sanitized_f, "fullpath": os.path.join(cwd, sanitized_f)}
|
||||||
data["sort"] = data["fullpath"].lower()
|
data["sort"] = data["fullpath"].lower()
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@ -1088,7 +1091,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() if y else y)
|
return config.set_from_dictionary(to_save, x, lambda y: y.strip().strip(u'\u200B\u200C\u200D\ufeff') if y else y)
|
||||||
|
|
||||||
|
|
||||||
def _configuration_gdrive_helper(to_save):
|
def _configuration_gdrive_helper(to_save):
|
||||||
@ -1162,7 +1165,6 @@ def _configuration_logfile_helper(to_save):
|
|||||||
|
|
||||||
def _configuration_ldap_helper(to_save):
|
def _configuration_ldap_helper(to_save):
|
||||||
reboot_required = False
|
reboot_required = False
|
||||||
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
|
|
||||||
reboot_required |= _config_int(to_save, "config_ldap_port")
|
reboot_required |= _config_int(to_save, "config_ldap_port")
|
||||||
reboot_required |= _config_int(to_save, "config_ldap_authentication")
|
reboot_required |= _config_int(to_save, "config_ldap_authentication")
|
||||||
reboot_required |= _config_string(to_save, "config_ldap_dn")
|
reboot_required |= _config_string(to_save, "config_ldap_dn")
|
||||||
@ -1178,9 +1180,14 @@ def _configuration_ldap_helper(to_save):
|
|||||||
reboot_required |= _config_string(to_save, "config_ldap_key_path")
|
reboot_required |= _config_string(to_save, "config_ldap_key_path")
|
||||||
_config_string(to_save, "config_ldap_group_name")
|
_config_string(to_save, "config_ldap_group_name")
|
||||||
_config_checkbox(to_save, "config_ldap_autocreate_user")
|
_config_checkbox(to_save, "config_ldap_autocreate_user")
|
||||||
if to_save.get("config_ldap_serv_password", "") != "":
|
|
||||||
|
address = urlparse(to_save.get("config_ldap_provider_url", ""))
|
||||||
|
to_save["config_ldap_provider_url"] = (address.hostname or address.path).strip("/")
|
||||||
|
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
|
||||||
|
|
||||||
|
if to_save.get("config_ldap_serv_password_e", "") != "":
|
||||||
reboot_required |= 1
|
reboot_required |= 1
|
||||||
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
|
config.set_from_dictionary(to_save, "config_ldap_serv_password_e")
|
||||||
config.save()
|
config.save()
|
||||||
|
|
||||||
if not config.config_ldap_provider_url \
|
if not config.config_ldap_provider_url \
|
||||||
@ -1192,7 +1199,7 @@ def _configuration_ldap_helper(to_save):
|
|||||||
|
|
||||||
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
||||||
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
||||||
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
|
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password_e):
|
||||||
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
|
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
|
||||||
else:
|
else:
|
||||||
if not config.config_ldap_serv_username:
|
if not config.config_ldap_serv_username:
|
||||||
@ -1256,16 +1263,16 @@ def new_user():
|
|||||||
content.default_language = config.config_default_language
|
content.default_language = config.config_default_language
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||||
config=config, translations=translations,
|
config=config, translations=translations,
|
||||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
languages=languages, title=_("Add New User"), page="newuser",
|
||||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings")
|
@admi.route("/admin/mailsettings", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_mailsettings():
|
def edit_mailsettings():
|
||||||
content = config.get_mail_settings()
|
content = config.get_mail_settings()
|
||||||
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
|
return render_title_template("email_edit.html", content=content, title=_("Edit Email Server Settings"),
|
||||||
page="mailset", feature_support=feature_support)
|
page="mailset", feature_support=feature_support)
|
||||||
|
|
||||||
|
|
||||||
@ -1284,7 +1291,7 @@ def update_mailsettings():
|
|||||||
elif to_save.get("gmail"):
|
elif to_save.get("gmail"):
|
||||||
try:
|
try:
|
||||||
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
|
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
|
||||||
flash(_(u"Gmail Account Verification Successful"), category="success")
|
flash(_("Success! Gmail Account Verified."), category="success")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
flash(str(ex), category="error")
|
flash(str(ex), category="error")
|
||||||
log.error(ex)
|
log.error(ex)
|
||||||
@ -1293,7 +1300,8 @@ def update_mailsettings():
|
|||||||
else:
|
else:
|
||||||
_config_int(to_save, "mail_port")
|
_config_int(to_save, "mail_port")
|
||||||
_config_int(to_save, "mail_use_ssl")
|
_config_int(to_save, "mail_use_ssl")
|
||||||
_config_string(to_save, "mail_password")
|
if to_save.get("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 = to_save.get('mail_server', "").strip()
|
||||||
config.mail_from = to_save.get('mail_from', "").strip()
|
config.mail_from = to_save.get('mail_from', "").strip()
|
||||||
@ -1303,24 +1311,24 @@ def update_mailsettings():
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
return edit_mailsettings()
|
return edit_mailsettings()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
return edit_mailsettings()
|
return edit_mailsettings()
|
||||||
|
|
||||||
if to_save.get("test"):
|
if to_save.get("test"):
|
||||||
if current_user.email:
|
if current_user.email:
|
||||||
result = send_test_mail(current_user.email, current_user.name)
|
result = send_test_mail(current_user.email, current_user.name)
|
||||||
if result is None:
|
if result is None:
|
||||||
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result",
|
flash(_("Test e-mail queued for sending to %(email)s, please check Tasks for result",
|
||||||
email=current_user.email), category="info")
|
email=current_user.email), category="info")
|
||||||
else:
|
else:
|
||||||
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
flash(_("There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
||||||
else:
|
else:
|
||||||
flash(_(u"Please configure your e-mail address first..."), category="error")
|
flash(_("Please configure your e-mail address first..."), category="error")
|
||||||
else:
|
else:
|
||||||
flash(_(u"E-mail server settings updated"), category="success")
|
flash(_("Email Server Settings updated"), category="success")
|
||||||
|
|
||||||
return edit_mailsettings()
|
return edit_mailsettings()
|
||||||
|
|
||||||
@ -1343,7 +1351,7 @@ def edit_scheduledtasks():
|
|||||||
config=content,
|
config=content,
|
||||||
starttime=time_field,
|
starttime=time_field,
|
||||||
duration=duration_field,
|
duration=duration_field,
|
||||||
title=_(u"Edit Scheduled Tasks Settings"))
|
title=_("Edit Scheduled Tasks Settings"))
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||||
@ -1355,21 +1363,22 @@ def update_scheduledtasks():
|
|||||||
if 0 <= int(to_save.get("schedule_start_time")) <= 23:
|
if 0 <= int(to_save.get("schedule_start_time")) <= 23:
|
||||||
_config_int( to_save, "schedule_start_time")
|
_config_int( to_save, "schedule_start_time")
|
||||||
else:
|
else:
|
||||||
flash(_(u"Invalid start time for task specified"), category="error")
|
flash(_("Invalid start time for task specified"), category="error")
|
||||||
error = True
|
error = True
|
||||||
if 0 < int(to_save.get("schedule_duration")) <= 60:
|
if 0 < int(to_save.get("schedule_duration")) <= 60:
|
||||||
_config_int(to_save, "schedule_duration")
|
_config_int(to_save, "schedule_duration")
|
||||||
else:
|
else:
|
||||||
flash(_(u"Invalid duration for task specified"), category="error")
|
flash(_("Invalid duration for task specified"), category="error")
|
||||||
error = True
|
error = True
|
||||||
_config_checkbox(to_save, "schedule_generate_book_covers")
|
_config_checkbox(to_save, "schedule_generate_book_covers")
|
||||||
_config_checkbox(to_save, "schedule_generate_series_covers")
|
_config_checkbox(to_save, "schedule_generate_series_covers")
|
||||||
|
_config_checkbox(to_save, "schedule_metadata_backup")
|
||||||
_config_checkbox(to_save, "schedule_reconnect")
|
_config_checkbox(to_save, "schedule_reconnect")
|
||||||
|
|
||||||
if not error:
|
if not error:
|
||||||
try:
|
try:
|
||||||
config.save()
|
config.save()
|
||||||
flash(_(u"Scheduled tasks settings updated"), category="success")
|
flash(_("Scheduled tasks settings updated"), category="success")
|
||||||
|
|
||||||
# Cancel any running tasks
|
# Cancel any running tasks
|
||||||
schedule.end_scheduled_tasks()
|
schedule.end_scheduled_tasks()
|
||||||
@ -1379,7 +1388,7 @@ def update_scheduledtasks():
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("An unknown error occurred while saving scheduled tasks settings")
|
log.error("An unknown error occurred while saving scheduled tasks settings")
|
||||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("Settings DB is not Writeable")
|
log.error("Settings DB is not Writeable")
|
||||||
@ -1394,7 +1403,7 @@ def update_scheduledtasks():
|
|||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||||
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
||||||
flash(_(u"User not found"), category="error")
|
flash(_("User not found"), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
languages = calibre_db.speaking_language(return_all_languages=True)
|
languages = calibre_db.speaking_language(return_all_languages=True)
|
||||||
translations = get_available_locale()
|
translations = get_available_locale()
|
||||||
@ -1413,7 +1422,7 @@ def edit_user(user_id):
|
|||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
mail_configured=config.get_mail_server_configured(),
|
mail_configured=config.get_mail_server_configured(),
|
||||||
kobo_support=kobo_support,
|
kobo_support=kobo_support,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
title=_("Edit User %(nick)s", nick=content.name),
|
||||||
page="edituser")
|
page="edituser")
|
||||||
|
|
||||||
|
|
||||||
@ -1424,14 +1433,14 @@ def reset_user_password(user_id):
|
|||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
ret, message = reset_password(user_id)
|
ret, message = reset_password(user_id)
|
||||||
if ret == 1:
|
if ret == 1:
|
||||||
log.debug(u"Password for user %s reset", message)
|
log.debug("Password for user %s reset", message)
|
||||||
flash(_(u"Password for user %(user)s reset", user=message), category="success")
|
flash(_("Success! Password for user %(user)s reset", user=message), category="success")
|
||||||
elif ret == 0:
|
elif ret == 0:
|
||||||
log.error(u"An unknown error occurred. Please try again later.")
|
log.error("An unknown error occurred. Please try again later.")
|
||||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||||
else:
|
else:
|
||||||
log.error(u"Please configure the SMTP mail settings first...")
|
log.error("Please configure the SMTP mail settings.")
|
||||||
flash(_(u"Please configure the SMTP mail settings first..."), category="error")
|
flash(_("Oops! Please configure the SMTP mail settings."), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
|
|
||||||
|
|
||||||
@ -1442,7 +1451,7 @@ def view_logfile():
|
|||||||
logfiles = {0: logger.get_logfile(config.config_logfile),
|
logfiles = {0: logger.get_logfile(config.config_logfile),
|
||||||
1: logger.get_accesslogfile(config.config_access_logfile)}
|
1: logger.get_accesslogfile(config.config_access_logfile)}
|
||||||
return render_title_template("logviewer.html",
|
return render_title_template("logviewer.html",
|
||||||
title=_(u"Logfile viewer"),
|
title=_("Logfile viewer"),
|
||||||
accesslog_enable=config.config_access_log,
|
accesslog_enable=config.config_access_log,
|
||||||
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
|
log_enable=bool(config.config_logfile != logger.LOG_TO_STDOUT),
|
||||||
logfiles=logfiles,
|
logfiles=logfiles,
|
||||||
@ -1492,7 +1501,7 @@ def download_debug():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def get_update_status():
|
def get_update_status():
|
||||||
if feature_support['updater']:
|
if feature_support['updater']:
|
||||||
log.info(u"Update status requested")
|
log.info("Update status requested")
|
||||||
return updater_thread.get_available_updates(request.method)
|
return updater_thread.get_available_updates(request.method)
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
@ -1687,7 +1696,7 @@ def _db_configuration_update_helper():
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
_db_configuration_result(_(u"Database error: %(error)s.", error=e.orig), gdrive_error)
|
_db_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig), gdrive_error)
|
||||||
try:
|
try:
|
||||||
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
|
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
|
||||||
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
||||||
@ -1719,7 +1728,7 @@ def _db_configuration_update_helper():
|
|||||||
_config_string(to_save, "config_calibre_dir")
|
_config_string(to_save, "config_calibre_dir")
|
||||||
calibre_db.update_config(config)
|
calibre_db.update_config(config)
|
||||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||||
flash(_(u"DB is not Writeable"), category="warning")
|
flash(_("DB is not Writeable"), category="warning")
|
||||||
config.save()
|
config.save()
|
||||||
return _db_configuration_result(None, gdrive_error)
|
return _db_configuration_result(None, gdrive_error)
|
||||||
|
|
||||||
@ -1776,10 +1785,11 @@ def _configuration_update_helper():
|
|||||||
# Goodreads configuration
|
# Goodreads configuration
|
||||||
_config_checkbox(to_save, "config_use_goodreads")
|
_config_checkbox(to_save, "config_use_goodreads")
|
||||||
_config_string(to_save, "config_goodreads_api_key")
|
_config_string(to_save, "config_goodreads_api_key")
|
||||||
_config_string(to_save, "config_goodreads_api_secret")
|
if to_save.get("config_goodreads_api_secret_e", ""):
|
||||||
|
_config_string(to_save, "config_goodreads_api_secret_e")
|
||||||
if services.goodreads_support:
|
if services.goodreads_support:
|
||||||
services.goodreads_support.connect(config.config_goodreads_api_key,
|
services.goodreads_support.connect(config.config_goodreads_api_key,
|
||||||
config.config_goodreads_api_secret,
|
config.config_goodreads_api_secret_e,
|
||||||
config.config_use_goodreads)
|
config.config_use_goodreads)
|
||||||
|
|
||||||
_config_int(to_save, "config_updatechannel")
|
_config_int(to_save, "config_updatechannel")
|
||||||
@ -1792,10 +1802,25 @@ def _configuration_update_helper():
|
|||||||
if config.config_login_type == constants.LOGIN_OAUTH:
|
if config.config_login_type == constants.LOGIN_OAUTH:
|
||||||
reboot_required |= _configuration_oauth_helper(to_save)
|
reboot_required |= _configuration_oauth_helper(to_save)
|
||||||
|
|
||||||
|
# logfile configuration
|
||||||
reboot, message = _configuration_logfile_helper(to_save)
|
reboot, message = _configuration_logfile_helper(to_save)
|
||||||
if message:
|
if message:
|
||||||
return message
|
return message
|
||||||
reboot_required |= reboot
|
reboot_required |= reboot
|
||||||
|
|
||||||
|
# security configuration
|
||||||
|
_config_checkbox(to_save, "config_password_policy")
|
||||||
|
_config_checkbox(to_save, "config_password_number")
|
||||||
|
_config_checkbox(to_save, "config_password_lower")
|
||||||
|
_config_checkbox(to_save, "config_password_upper")
|
||||||
|
_config_checkbox(to_save, "config_password_special")
|
||||||
|
if 0 < int(to_save.get("config_password_min_length", "0")) < 41:
|
||||||
|
_config_int(to_save, "config_password_min_length")
|
||||||
|
else:
|
||||||
|
return _configuration_result(_('Password length has to be between 1 and 40'))
|
||||||
|
reboot_required |= _config_int(to_save, "config_session")
|
||||||
|
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
|
||||||
|
|
||||||
# Rarfile Content configuration
|
# Rarfile Content configuration
|
||||||
_config_string(to_save, "config_rarfile_location")
|
_config_string(to_save, "config_rarfile_location")
|
||||||
if "config_rarfile_location" in to_save:
|
if "config_rarfile_location" in to_save:
|
||||||
@ -1805,7 +1830,7 @@ def _configuration_update_helper():
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
_configuration_result(_(u"Database error: %(error)s.", error=e.orig))
|
_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig))
|
||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
if reboot_required:
|
if reboot_required:
|
||||||
@ -1821,7 +1846,7 @@ def _configuration_result(error_flash=None, reboot=False):
|
|||||||
config.load()
|
config.load()
|
||||||
resp['result'] = [{'type': "danger", 'message': error_flash}]
|
resp['result'] = [{'type': "danger", 'message': error_flash}]
|
||||||
else:
|
else:
|
||||||
resp['result'] = [{'type': "success", 'message': _(u"Calibre-Web configuration updated")}]
|
resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
|
||||||
resp['reboot'] = reboot
|
resp['reboot'] = reboot
|
||||||
resp['config_upload'] = config.config_upload_formats
|
resp['config_upload'] = config.config_upload_formats
|
||||||
return Response(json.dumps(resp), mimetype='application/json')
|
return Response(json.dumps(resp), mimetype='application/json')
|
||||||
@ -1852,7 +1877,7 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
|
|||||||
gdriveError=gdrive_error,
|
gdriveError=gdrive_error,
|
||||||
gdrivefolders=gdrivefolders,
|
gdrivefolders=gdrivefolders,
|
||||||
feature_support=feature_support,
|
feature_support=feature_support,
|
||||||
title=_(u"Database Configuration"), page="dbconfig")
|
title=_("Database Configuration"), page="dbconfig")
|
||||||
|
|
||||||
|
|
||||||
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||||
@ -1864,11 +1889,11 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content.sidebar_view |= constants.DETAIL_RANDOM
|
content.sidebar_view |= constants.DETAIL_RANDOM
|
||||||
|
|
||||||
content.role = constants.selected_roles(to_save)
|
content.role = constants.selected_roles(to_save)
|
||||||
content.password = generate_password_hash(to_save["password"])
|
|
||||||
try:
|
try:
|
||||||
if not to_save["name"] or not to_save["email"] or not to_save["password"]:
|
if not to_save["name"] or not to_save["email"] or not to_save["password"]:
|
||||||
log.info("Missing entries on new user")
|
log.info("Missing entries on new user")
|
||||||
raise Exception(_(u"Please fill out all fields!"))
|
raise Exception(_("Oops! Please complete all fields."))
|
||||||
|
content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
|
||||||
content.email = check_email(to_save["email"])
|
content.email = check_email(to_save["email"])
|
||||||
# Query username, if not existing, change
|
# Query username, if not existing, change
|
||||||
content.name = check_username(to_save["name"])
|
content.name = check_username(to_save["name"])
|
||||||
@ -1876,13 +1901,13 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content.kindle_mail = valid_email(to_save["kindle_mail"])
|
content.kindle_mail = valid_email(to_save["kindle_mail"])
|
||||||
if config.config_public_reg and not check_valid_domain(content.email):
|
if config.config_public_reg and not check_valid_domain(content.email):
|
||||||
log.info("E-mail: {} for new user is not from valid domain".format(content.email))
|
log.info("E-mail: {} for new user is not from valid domain".format(content.email))
|
||||||
raise Exception(_(u"E-mail is not from valid domain"))
|
raise Exception(_("E-mail is not from valid domain"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
flash(str(ex), category="error")
|
flash(str(ex), category="error")
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||||
config=config,
|
config=config,
|
||||||
translations=translations,
|
translations=translations,
|
||||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
languages=languages, title=_("Add new user"), page="newuser",
|
||||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
try:
|
try:
|
||||||
content.allowed_tags = config.config_allowed_tags
|
content.allowed_tags = config.config_allowed_tags
|
||||||
@ -1893,17 +1918,17 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
|
content.kobo_only_shelves_sync = to_save.get("kobo_only_shelves_sync", 0) == "on"
|
||||||
ub.session.add(content)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"User '%(user)s' created", user=content.name), category="success")
|
flash(_("User '%(user)s' created", user=content.name), category="success")
|
||||||
log.debug("User {} created".format(content.name))
|
log.debug("User {} created".format(content.name))
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("Found an existing account for {} or {}".format(content.name, content.email))
|
log.error("Found an existing account for {} or {}".format(content.name, content.email))
|
||||||
flash(_("Found an existing account for this e-mail address or name."), category="error")
|
flash(_("Oops! An account already exists for this Email. or name."), category="error")
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
|
|
||||||
|
|
||||||
def _delete_user(content):
|
def _delete_user(content):
|
||||||
@ -1931,10 +1956,10 @@ def _delete_user(content):
|
|||||||
log.info("User {} deleted".format(content.name))
|
log.info("User {} deleted".format(content.name))
|
||||||
return _("User '%(nick)s' deleted", nick=content.name)
|
return _("User '%(nick)s' deleted", nick=content.name)
|
||||||
else:
|
else:
|
||||||
log.warning(_("Can't delete Guest User"))
|
# log.warning(_("Can't delete Guest User"))
|
||||||
raise Exception(_("Can't delete Guest User"))
|
raise Exception(_("Can't delete Guest User"))
|
||||||
else:
|
else:
|
||||||
log.warning("No admin user remaining, can't delete user")
|
# log.warning("No admin user remaining, can't delete user")
|
||||||
raise Exception(_("No admin user remaining, can't delete user"))
|
raise Exception(_("No admin user remaining, can't delete user"))
|
||||||
|
|
||||||
|
|
||||||
@ -1952,14 +1977,6 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
log.warning("No admin user remaining, can't remove admin role from {}".format(content.name))
|
log.warning("No admin user remaining, can't remove admin role from {}".format(content.name))
|
||||||
flash(_("No admin user remaining, can't remove admin role"), category="error")
|
flash(_("No admin user remaining, can't remove admin role"), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
if to_save.get("password"):
|
|
||||||
content.password = generate_password_hash(to_save["password"])
|
|
||||||
anonymous = content.is_anonymous
|
|
||||||
content.role = constants.selected_roles(to_save)
|
|
||||||
if anonymous:
|
|
||||||
content.role |= constants.ROLE_ANONYMOUS
|
|
||||||
else:
|
|
||||||
content.role &= ~constants.ROLE_ANONYMOUS
|
|
||||||
|
|
||||||
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
|
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
|
||||||
sidebar, __ = get_sidebar_config()
|
sidebar, __ = get_sidebar_config()
|
||||||
@ -1987,9 +2004,18 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
if to_save.get("locale"):
|
if to_save.get("locale"):
|
||||||
content.locale = to_save["locale"]
|
content.locale = to_save["locale"]
|
||||||
try:
|
try:
|
||||||
|
anonymous = content.is_anonymous
|
||||||
|
content.role = constants.selected_roles(to_save)
|
||||||
|
if anonymous:
|
||||||
|
content.role |= constants.ROLE_ANONYMOUS
|
||||||
|
else:
|
||||||
|
content.role &= ~constants.ROLE_ANONYMOUS
|
||||||
|
if to_save.get("password", ""):
|
||||||
|
content.password = generate_password_hash(helper.valid_password(to_save.get("password", "")))
|
||||||
|
|
||||||
new_email = valid_email(to_save.get("email", content.email))
|
new_email = valid_email(to_save.get("email", content.email))
|
||||||
if not new_email:
|
if not new_email:
|
||||||
raise Exception(_(u"E-Mail Address can't be empty and has to be a valid E-Mail"))
|
raise Exception(_("Email can't be empty and has to be a valid Email"))
|
||||||
if new_email != content.email:
|
if new_email != content.email:
|
||||||
content.email = check_email(new_email)
|
content.email = check_email(new_email)
|
||||||
# Query username, if not existing, change
|
# Query username, if not existing, change
|
||||||
@ -2011,19 +2037,19 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content=content,
|
content=content,
|
||||||
config=config,
|
config=config,
|
||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
title=_("Edit User %(nick)s", nick=content.name),
|
||||||
page="edituser")
|
page="edituser")
|
||||||
try:
|
try:
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
|
flash(_("User '%(nick)s' updated", nick=content.name), category="success")
|
||||||
except IntegrityError as ex:
|
except IntegrityError as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
|
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
|
||||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
flash(_("Oops! An unknown error occurred. Please try again later."), category="error")
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
13
cps/babel.py
13
cps/babel.py
@ -1,7 +1,8 @@
|
|||||||
from babel import negotiate_locale
|
from babel import negotiate_locale
|
||||||
from flask_babel import Babel, Locale
|
from flask_babel import Babel, Locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import request, g
|
from flask import request
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
@ -9,14 +10,12 @@ log = logger.create()
|
|||||||
|
|
||||||
babel = Babel()
|
babel = Babel()
|
||||||
|
|
||||||
|
|
||||||
@babel.localeselector
|
|
||||||
def get_locale():
|
def get_locale():
|
||||||
# if a user is logged in, use the locale from the user settings
|
# if a user is logged in, use the locale from the user settings
|
||||||
user = getattr(g, 'user', None)
|
if current_user is not None and hasattr(current_user, "locale"):
|
||||||
if user is not None and hasattr(user, "locale"):
|
# if the account is the guest account bypass the config lang settings
|
||||||
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
if current_user.name != 'Guest':
|
||||||
return user.locale
|
return current_user.locale
|
||||||
|
|
||||||
preferred = list()
|
preferred = list()
|
||||||
if request.accept_languages:
|
if request.accept_languages:
|
||||||
|
@ -48,6 +48,7 @@ class CliParameter(object):
|
|||||||
'works only in combination with keyfile')
|
'works only in combination with keyfile')
|
||||||
parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, '
|
parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, '
|
||||||
'works only in combination with certfile')
|
'works only in combination with certfile')
|
||||||
|
parser.add_argument('-o', metavar='path', help='path and name Calibre-Web logfile')
|
||||||
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
|
||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||||
@ -60,6 +61,7 @@ class CliParameter(object):
|
|||||||
parser.add_argument('-r', action='store_true', help='Enable public database reconnect route under /reconnect')
|
parser.add_argument('-r', action='store_true', help='Enable public database reconnect route under /reconnect')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
self.logpath = args.o or ""
|
||||||
self.settings_path = args.p or os.path.join(_CONFIG_DIR, DEFAULT_SETTINGS_FILE)
|
self.settings_path = args.p or os.path.join(_CONFIG_DIR, DEFAULT_SETTINGS_FILE)
|
||||||
self.gd_path = args.g or os.path.join(_CONFIG_DIR, DEFAULT_GDRIVE_FILE)
|
self.gd_path = args.g or os.path.join(_CONFIG_DIR, DEFAULT_GDRIVE_FILE)
|
||||||
|
|
||||||
|
58
cps/comic.py
58
cps/comic.py
@ -36,6 +36,12 @@ try:
|
|||||||
from comicapi import __version__ as comic_version
|
from comicapi import __version__ as comic_version
|
||||||
except ImportError:
|
except ImportError:
|
||||||
comic_version = ''
|
comic_version = ''
|
||||||
|
try:
|
||||||
|
from comicapi.comicarchive import load_archive_plugins
|
||||||
|
import comicapi.utils
|
||||||
|
comicapi.utils.add_rar_paths()
|
||||||
|
except ImportError:
|
||||||
|
load_archive_plugins = None
|
||||||
except (ImportError, LookupError) as e:
|
except (ImportError, LookupError) as e:
|
||||||
log.debug('Cannot import comicapi, extracting comic metadata will not work: %s', e)
|
log.debug('Cannot import comicapi, extracting comic metadata will not work: %s', e)
|
||||||
import zipfile
|
import zipfile
|
||||||
@ -46,6 +52,12 @@ except (ImportError, LookupError) as e:
|
|||||||
except (ImportError, SyntaxError) as e:
|
except (ImportError, SyntaxError) as e:
|
||||||
log.debug('Cannot import rarfile, extracting cover files from rar files will not work: %s', e)
|
log.debug('Cannot import rarfile, extracting cover files from rar files will not work: %s', e)
|
||||||
use_rarfile = False
|
use_rarfile = False
|
||||||
|
try:
|
||||||
|
import py7zr
|
||||||
|
use_7zip = True
|
||||||
|
except (ImportError, SyntaxError) as e:
|
||||||
|
log.debug('Cannot import py7zr, extracting cover files from CB7 files will not work: %s', e)
|
||||||
|
use_7zip = False
|
||||||
use_comic_meta = False
|
use_comic_meta = False
|
||||||
|
|
||||||
|
|
||||||
@ -78,23 +90,40 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec
|
|||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in cover.COVER_EXTENSIONS:
|
if extension in cover.COVER_EXTENSIONS:
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read([name])
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.debug('Rarfile failed with error: {}'.format(ex))
|
log.error('Rarfile failed with error: {}'.format(ex))
|
||||||
|
elif original_file_extension.upper() == '.CB7' and use_7zip:
|
||||||
|
cf = py7zr.SevenZipFile(tmp_file_name)
|
||||||
|
for name in cf.getnames():
|
||||||
|
ext = os.path.splitext(name)
|
||||||
|
if len(ext) > 1:
|
||||||
|
extension = ext[1].lower()
|
||||||
|
if extension in cover.COVER_EXTENSIONS:
|
||||||
|
try:
|
||||||
|
cover_data = cf.read(name)[name].read()
|
||||||
|
except (py7zr.Bad7zFile, OSError) as ex:
|
||||||
|
log.error('7Zip file failed with error: {}'.format(ex))
|
||||||
|
break
|
||||||
return cover_data, extension
|
return cover_data, extension
|
||||||
|
|
||||||
|
|
||||||
def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
||||||
cover_data = extension = None
|
cover_data = extension = None
|
||||||
if use_comic_meta:
|
if use_comic_meta:
|
||||||
|
try:
|
||||||
archive = ComicArchive(tmp_file_name, rar_exe_path=rar_executable)
|
archive = ComicArchive(tmp_file_name, rar_exe_path=rar_executable)
|
||||||
for index, name in enumerate(archive.getPageNameList()):
|
except TypeError:
|
||||||
|
archive = ComicArchive(tmp_file_name)
|
||||||
|
name_list = archive.getPageNameList if hasattr(archive, "getPageNameList") else archive.get_page_name_list
|
||||||
|
for index, name in enumerate(name_list()):
|
||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in cover.COVER_EXTENSIONS:
|
if extension in cover.COVER_EXTENSIONS:
|
||||||
cover_data = archive.getPage(index)
|
get_page = archive.getPage if hasattr(archive, "getPageNameList") else archive.get_page
|
||||||
|
cover_data = get_page(index)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
cover_data, extension = _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_executable)
|
cover_data, extension = _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_executable)
|
||||||
@ -103,17 +132,26 @@ def _extract_cover(tmp_file_name, original_file_extension, rar_executable):
|
|||||||
|
|
||||||
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):
|
||||||
if use_comic_meta:
|
if use_comic_meta:
|
||||||
|
try:
|
||||||
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
|
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
|
||||||
if archive.seemsToBeAComicArchive():
|
except TypeError:
|
||||||
if archive.hasMetadata(MetaDataStyle.CIX):
|
load_archive_plugins(force=True, rar=rar_executable)
|
||||||
|
archive = ComicArchive(tmp_file_path)
|
||||||
|
if hasattr(archive, "seemsToBeAComicArchive"):
|
||||||
|
seems_archive = archive.seemsToBeAComicArchive
|
||||||
|
else:
|
||||||
|
seems_archive = archive.seems_to_be_a_comic_archive
|
||||||
|
if seems_archive():
|
||||||
|
has_metadata = archive.hasMetadata if hasattr(archive, "hasMetadata") else archive.has_metadata
|
||||||
|
if has_metadata(MetaDataStyle.CIX):
|
||||||
style = MetaDataStyle.CIX
|
style = MetaDataStyle.CIX
|
||||||
elif archive.hasMetadata(MetaDataStyle.CBI):
|
elif has_metadata(MetaDataStyle.CBI):
|
||||||
style = MetaDataStyle.CBI
|
style = MetaDataStyle.CBI
|
||||||
else:
|
else:
|
||||||
style = None
|
style = None
|
||||||
|
|
||||||
# if style is not None:
|
read_metadata = archive.readMetadata if hasattr(archive, "readMetadata") else archive.read_metadata
|
||||||
loaded_metadata = archive.readMetadata(style)
|
loaded_metadata = read_metadata(style)
|
||||||
|
|
||||||
lang = loaded_metadata.language or ""
|
lang = loaded_metadata.language or ""
|
||||||
loaded_metadata.language = isoLanguages.get_lang3(lang)
|
loaded_metadata.language = isoLanguages.get_lang3(lang)
|
||||||
@ -138,7 +176,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=original_file_name,
|
title=original_file_name,
|
||||||
author=u'Unknown',
|
author='Unknown',
|
||||||
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
|
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable),
|
||||||
description="",
|
description="",
|
||||||
tags="",
|
tags="",
|
||||||
|
@ -23,6 +23,10 @@ import json
|
|||||||
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.sql.expression import text
|
from sqlalchemy.sql.expression import text
|
||||||
|
from sqlalchemy import exists
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
import cryptography.exceptions
|
||||||
|
from base64 import urlsafe_b64decode
|
||||||
try:
|
try:
|
||||||
# Compatibility with sqlalchemy 2.0
|
# Compatibility with sqlalchemy 2.0
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base
|
||||||
@ -56,7 +60,8 @@ class _Settings(_Base):
|
|||||||
mail_port = Column(Integer, default=25)
|
mail_port = Column(Integer, default=25)
|
||||||
mail_use_ssl = Column(SmallInteger, default=0)
|
mail_use_ssl = Column(SmallInteger, default=0)
|
||||||
mail_login = Column(String, default='mail@example.com')
|
mail_login = Column(String, default='mail@example.com')
|
||||||
mail_password = Column(String, default='mypassword')
|
mail_password_e = Column(String)
|
||||||
|
mail_password = Column(String)
|
||||||
mail_from = Column(String, default='automailer <mail@example.com>')
|
mail_from = Column(String, default='automailer <mail@example.com>')
|
||||||
mail_size = Column(Integer, default=25*1024*1024)
|
mail_size = Column(Integer, default=25*1024*1024)
|
||||||
mail_server_type = Column(SmallInteger, default=0)
|
mail_server_type = Column(SmallInteger, default=0)
|
||||||
@ -69,19 +74,18 @@ class _Settings(_Base):
|
|||||||
config_certfile = Column(String)
|
config_certfile = Column(String)
|
||||||
config_keyfile = Column(String)
|
config_keyfile = Column(String)
|
||||||
config_trustedhosts = Column(String, default='')
|
config_trustedhosts = Column(String, default='')
|
||||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
config_calibre_web_title = Column(String, default='Calibre-Web')
|
||||||
config_books_per_page = Column(Integer, default=60)
|
config_books_per_page = Column(Integer, default=60)
|
||||||
config_random_books = Column(Integer, default=4)
|
config_random_books = Column(Integer, default=4)
|
||||||
config_authors_max = Column(Integer, default=0)
|
config_authors_max = Column(Integer, default=0)
|
||||||
config_read_column = Column(Integer, default=0)
|
config_read_column = Column(Integer, default=0)
|
||||||
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')
|
||||||
# config_mature_content_tags = Column(String, default='')
|
|
||||||
config_theme = Column(Integer, default=0)
|
config_theme = Column(Integer, default=0)
|
||||||
|
|
||||||
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
||||||
config_logfile = Column(String)
|
config_logfile = Column(String, default=logger.DEFAULT_LOG_FILE)
|
||||||
config_access_log = Column(SmallInteger, default=0)
|
config_access_log = Column(SmallInteger, default=0)
|
||||||
config_access_logfile = Column(String)
|
config_access_logfile = Column(String, default=logger.DEFAULT_ACCESS_LOG)
|
||||||
|
|
||||||
config_uploading = Column(SmallInteger, default=0)
|
config_uploading = Column(SmallInteger, default=0)
|
||||||
config_anonbrowse = Column(SmallInteger, default=0)
|
config_anonbrowse = Column(SmallInteger, default=0)
|
||||||
@ -107,6 +111,7 @@ class _Settings(_Base):
|
|||||||
|
|
||||||
config_use_goodreads = Column(Boolean, default=False)
|
config_use_goodreads = Column(Boolean, default=False)
|
||||||
config_goodreads_api_key = Column(String)
|
config_goodreads_api_key = Column(String)
|
||||||
|
config_goodreads_api_secret_e = Column(String)
|
||||||
config_goodreads_api_secret = Column(String)
|
config_goodreads_api_secret = Column(String)
|
||||||
config_register_email = Column(Boolean, default=False)
|
config_register_email = Column(Boolean, default=False)
|
||||||
config_login_type = Column(Integer, default=0)
|
config_login_type = Column(Integer, default=0)
|
||||||
@ -117,7 +122,8 @@ class _Settings(_Base):
|
|||||||
config_ldap_port = Column(SmallInteger, default=389)
|
config_ldap_port = Column(SmallInteger, default=389)
|
||||||
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
|
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
|
||||||
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
|
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
|
||||||
config_ldap_serv_password = Column(String, default="")
|
config_ldap_serv_password_e = Column(String)
|
||||||
|
config_ldap_serv_password = Column(String)
|
||||||
config_ldap_encryption = Column(SmallInteger, default=0)
|
config_ldap_encryption = Column(SmallInteger, default=0)
|
||||||
config_ldap_cacert_path = Column(String, default="")
|
config_ldap_cacert_path = Column(String, default="")
|
||||||
config_ldap_cert_path = Column(String, default="")
|
config_ldap_cert_path = Column(String, default="")
|
||||||
@ -148,24 +154,35 @@ class _Settings(_Base):
|
|||||||
schedule_generate_book_covers = Column(Boolean, default=False)
|
schedule_generate_book_covers = Column(Boolean, default=False)
|
||||||
schedule_generate_series_covers = Column(Boolean, default=False)
|
schedule_generate_series_covers = Column(Boolean, default=False)
|
||||||
schedule_reconnect = Column(Boolean, default=False)
|
schedule_reconnect = Column(Boolean, default=False)
|
||||||
|
schedule_metadata_backup = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
config_password_policy = Column(Boolean, default=True)
|
||||||
|
config_password_min_length = Column(Integer, default=8)
|
||||||
|
config_password_number = Column(Boolean, default=True)
|
||||||
|
config_password_lower = Column(Boolean, default=True)
|
||||||
|
config_password_upper = Column(Boolean, default=True)
|
||||||
|
config_password_special = Column(Boolean, default=True)
|
||||||
|
config_session = Column(Integer, default=1)
|
||||||
|
config_ratelimiter = Column(Boolean, default=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
# Class holds all application specific settings in calibre-web
|
# Class holds all application specific settings in calibre-web
|
||||||
class _ConfigSQL(object):
|
class ConfigSQL(object):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.__dict__["dirty"] = list()
|
||||||
|
|
||||||
def init_config(self, session, cli):
|
def init_config(self, session, secret_key, cli):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._settings = None
|
self._settings = None
|
||||||
self.db_configured = None
|
self.db_configured = None
|
||||||
self.config_calibre_dir = None
|
self.config_calibre_dir = None
|
||||||
self.load()
|
self._fernet = Fernet(secret_key)
|
||||||
self.cli = cli
|
self.cli = cli
|
||||||
|
self.load()
|
||||||
|
|
||||||
change = False
|
change = False
|
||||||
if self.config_converterpath == None: # pylint: disable=access-member-before-definition
|
if self.config_converterpath == None: # pylint: disable=access-member-before-definition
|
||||||
@ -294,10 +311,10 @@ class _ConfigSQL(object):
|
|||||||
setattr(self, field, new_value)
|
setattr(self, field, new_value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def toDict(self):
|
def to_dict(self):
|
||||||
storage = {}
|
storage = {}
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli":
|
if k[0] != '_' and not k.endswith("_e") and not k == "cli":
|
||||||
storage[k] = v
|
storage[k] = v
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
@ -311,6 +328,12 @@ class _ConfigSQL(object):
|
|||||||
column = s.__class__.__dict__.get(k)
|
column = s.__class__.__dict__.get(k)
|
||||||
if column.default is not None:
|
if column.default is not None:
|
||||||
v = column.default.arg
|
v = column.default.arg
|
||||||
|
if k.endswith("_e") and v is not None:
|
||||||
|
try:
|
||||||
|
setattr(self, k, self._fernet.decrypt(v).decode())
|
||||||
|
except cryptography.fernet.InvalidToken:
|
||||||
|
setattr(self, k, "")
|
||||||
|
else:
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
have_metadata_db = bool(self.config_calibre_dir)
|
have_metadata_db = bool(self.config_calibre_dir)
|
||||||
@ -319,30 +342,37 @@ class _ConfigSQL(object):
|
|||||||
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(',')]
|
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
|
||||||
|
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)
|
||||||
else:
|
else:
|
||||||
# pylint: disable=access-member-before-definition
|
# pylint: disable=access-member-before-definition
|
||||||
logfile = logger.setup(self.config_logfile, self.config_log_level)
|
logfile = logger.setup(cli_param.logpath or self.config_logfile, self.config_log_level)
|
||||||
if logfile != self.config_logfile:
|
if logfile != os.path.abspath(self.config_logfile):
|
||||||
|
if logfile != os.path.abspath(cli_param.logpath):
|
||||||
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
||||||
self.config_logfile = logfile
|
self.config_logfile = logfile
|
||||||
|
s.config_logfile = logfile
|
||||||
self._session.merge(s)
|
self._session.merge(s)
|
||||||
try:
|
try:
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
log.error('Database error: %s', e)
|
log.error('Database error: %s', e)
|
||||||
self._session.rollback()
|
self._session.rollback()
|
||||||
|
self.__dict__["dirty"] = list()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Apply all configuration values to the underlying storage."""
|
"""Apply all configuration values to the underlying storage."""
|
||||||
s = self._read_from_storage() # type: _Settings
|
s = self._read_from_storage() # type: _Settings
|
||||||
|
|
||||||
for k, v in self.__dict__.items():
|
for k in self.dirty:
|
||||||
if k[0] == '_':
|
if k[0] == '_':
|
||||||
continue
|
continue
|
||||||
if hasattr(s, k):
|
if hasattr(s, k):
|
||||||
setattr(s, k, v)
|
if k.endswith("_e"):
|
||||||
|
setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode()))
|
||||||
|
else:
|
||||||
|
setattr(s, k, self.__dict__[k])
|
||||||
|
|
||||||
log.debug("_ConfigSQL updating storage")
|
log.debug("_ConfigSQL updating storage")
|
||||||
self._session.merge(s)
|
self._session.merge(s)
|
||||||
@ -358,7 +388,6 @@ class _ConfigSQL(object):
|
|||||||
log.error(error)
|
log.error(error)
|
||||||
log.warning("invalidating configuration")
|
log.warning("invalidating configuration")
|
||||||
self.db_configured = False
|
self.db_configured = False
|
||||||
# self.config_calibre_dir = None
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def store_calibre_uuid(self, calibre_db, Library_table):
|
def store_calibre_uuid(self, calibre_db, Library_table):
|
||||||
@ -370,8 +399,40 @@ class _ConfigSQL(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def __setattr__(self, attr_name, attr_value):
|
||||||
|
super().__setattr__(attr_name, attr_value)
|
||||||
|
self.__dict__["dirty"].append(attr_name)
|
||||||
|
|
||||||
def _migrate_table(session, orm_class):
|
|
||||||
|
def _encrypt_fields(session, secret_key):
|
||||||
|
try:
|
||||||
|
session.query(exists().where(_Settings.mail_password_e)).scalar()
|
||||||
|
except OperationalError:
|
||||||
|
with session.bind.connect() as conn:
|
||||||
|
conn.execute(text("ALTER TABLE settings ADD column 'mail_password_e' String"))
|
||||||
|
conn.execute(text("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String"))
|
||||||
|
conn.execute(text("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String"))
|
||||||
|
session.commit()
|
||||||
|
crypter = Fernet(secret_key)
|
||||||
|
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
|
||||||
|
_Settings.config_ldap_serv_password).first()
|
||||||
|
if settings.mail_password:
|
||||||
|
session.query(_Settings).update(
|
||||||
|
{_Settings.mail_password_e: crypter.encrypt(settings.mail_password.encode())})
|
||||||
|
if settings.config_goodreads_api_secret:
|
||||||
|
session.query(_Settings).update(
|
||||||
|
{_Settings.config_goodreads_api_secret_e:
|
||||||
|
crypter.encrypt(settings.config_goodreads_api_secret.encode())})
|
||||||
|
if settings.config_ldap_serv_password:
|
||||||
|
session.query(_Settings).update(
|
||||||
|
{_Settings.config_ldap_serv_password_e:
|
||||||
|
crypter.encrypt(settings.config_ldap_serv_password.encode())})
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_table(session, orm_class, secret_key=None):
|
||||||
|
if secret_key:
|
||||||
|
_encrypt_fields(session, secret_key)
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
for column_name, column in orm_class.__dict__.items():
|
for column_name, column in orm_class.__dict__.items():
|
||||||
@ -447,22 +508,18 @@ def autodetect_kepubify_binary():
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _migrate_database(session):
|
def _migrate_database(session, secret_key):
|
||||||
# make sure the table is created, if it does not exist
|
# make sure the table is created, if it does not exist
|
||||||
_Base.metadata.create_all(session.bind)
|
_Base.metadata.create_all(session.bind)
|
||||||
_migrate_table(session, _Settings)
|
_migrate_table(session, _Settings, secret_key)
|
||||||
_migrate_table(session, _Flask_Settings)
|
_migrate_table(session, _Flask_Settings)
|
||||||
|
|
||||||
|
|
||||||
def load_configuration(conf, session, cli):
|
def load_configuration(session, secret_key):
|
||||||
_migrate_database(session)
|
_migrate_database(session, secret_key)
|
||||||
|
|
||||||
if not session.query(_Settings).count():
|
if not session.query(_Settings).count():
|
||||||
session.add(_Settings())
|
session.add(_Settings())
|
||||||
session.commit()
|
session.commit()
|
||||||
# conf = _ConfigSQL()
|
|
||||||
conf.init_config(session, cli)
|
|
||||||
# return conf
|
|
||||||
|
|
||||||
|
|
||||||
def get_flask_session_key(_session):
|
def get_flask_session_key(_session):
|
||||||
@ -472,3 +529,25 @@ def get_flask_session_key(_session):
|
|||||||
_session.add(flask_settings)
|
_session.add(flask_settings)
|
||||||
_session.commit()
|
_session.commit()
|
||||||
return flask_settings.flask_session_key
|
return flask_settings.flask_session_key
|
||||||
|
|
||||||
|
|
||||||
|
def get_encryption_key(key_path):
|
||||||
|
key_file = os.path.join(key_path, ".key")
|
||||||
|
generate = True
|
||||||
|
error = ""
|
||||||
|
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
|
||||||
|
with open(key_file, "rb") as f:
|
||||||
|
key = f.read()
|
||||||
|
try:
|
||||||
|
urlsafe_b64decode(key)
|
||||||
|
generate = False
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if generate:
|
||||||
|
key = Fernet.generate_key()
|
||||||
|
try:
|
||||||
|
with open(key_file, "wb") as f:
|
||||||
|
f.write(key)
|
||||||
|
except PermissionError as e:
|
||||||
|
error = e
|
||||||
|
return key, error
|
||||||
|
@ -34,6 +34,8 @@ UPDATER_AVAILABLE = True
|
|||||||
|
|
||||||
# Base dir is parent of current file, necessary if called from different folder
|
# Base dir is parent of current file, necessary if called from different folder
|
||||||
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
|
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
|
||||||
|
# if executable file the files should be placed in the parent dir (parallel to the exe file)
|
||||||
|
|
||||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||||
@ -49,6 +51,9 @@ if HOME_CONFIG:
|
|||||||
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir)
|
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir)
|
||||||
else:
|
else:
|
||||||
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
|
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
CONFIG_DIR = os.path.abspath(os.path.join(CONFIG_DIR, os.pardir))
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_SETTINGS_FILE = "app.db"
|
DEFAULT_SETTINGS_FILE = "app.db"
|
||||||
DEFAULT_GDRIVE_FILE = "gdrive.db"
|
DEFAULT_GDRIVE_FILE = "gdrive.db"
|
||||||
@ -144,10 +149,10 @@ del env_CALIBRE_PORT
|
|||||||
|
|
||||||
EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
||||||
EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf',
|
EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf',
|
||||||
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr']
|
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr', 'prc']
|
||||||
EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
|
EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
|
||||||
'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt']
|
'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt']
|
||||||
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu',
|
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'cb7', 'djvu', 'djv',
|
||||||
'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg',
|
'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg',
|
||||||
'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
'opus', 'wav', 'flac', 'm4a', 'm4b'}
|
||||||
|
|
||||||
@ -163,7 +168,8 @@ def selected_roles(dictionary):
|
|||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages, publisher, pubdate, identifiers')
|
'series_id, languages, publisher, pubdate, identifiers')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.19'}
|
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||||
|
STABLE_VERSION = {'version': '0.6.22 Beta'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = dict()
|
NIGHTLY_VERSION = dict()
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
117
cps/db.py
117
cps/db.py
@ -111,66 +111,73 @@ class Identifiers(Base):
|
|||||||
def format_type(self):
|
def format_type(self):
|
||||||
format_type = self.type.lower()
|
format_type = self.type.lower()
|
||||||
if format_type == 'amazon':
|
if format_type == 'amazon':
|
||||||
return u"Amazon"
|
return "Amazon"
|
||||||
elif format_type.startswith("amazon_"):
|
elif format_type.startswith("amazon_"):
|
||||||
return u"Amazon.{0}".format(format_type[7:])
|
return "Amazon.{0}".format(format_type[7:])
|
||||||
elif format_type == "isbn":
|
elif format_type == "isbn":
|
||||||
return u"ISBN"
|
return "ISBN"
|
||||||
elif format_type == "doi":
|
elif format_type == "doi":
|
||||||
return u"DOI"
|
return "DOI"
|
||||||
elif format_type == "douban":
|
elif format_type == "douban":
|
||||||
return u"Douban"
|
return "Douban"
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"Goodreads"
|
return "Goodreads"
|
||||||
elif format_type == "babelio":
|
elif format_type == "babelio":
|
||||||
return u"Babelio"
|
return "Babelio"
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
return u"Google Books"
|
return "Google Books"
|
||||||
elif format_type == "kobo":
|
elif format_type == "kobo":
|
||||||
return u"Kobo"
|
return "Kobo"
|
||||||
elif format_type == "litres":
|
elif format_type == "litres":
|
||||||
return u"ЛитРес"
|
return "ЛитРес"
|
||||||
elif format_type == "issn":
|
elif format_type == "issn":
|
||||||
return u"ISSN"
|
return "ISSN"
|
||||||
elif format_type == "isfdb":
|
elif format_type == "isfdb":
|
||||||
return u"ISFDB"
|
return "ISFDB"
|
||||||
if format_type == "lubimyczytac":
|
if format_type == "lubimyczytac":
|
||||||
return u"Lubimyczytac"
|
return "Lubimyczytac"
|
||||||
|
if format_type == "databazeknih":
|
||||||
|
return "Databáze knih"
|
||||||
else:
|
else:
|
||||||
return self.type
|
return self.type
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
format_type = self.type.lower()
|
format_type = self.type.lower()
|
||||||
if format_type == "amazon" or format_type == "asin":
|
if format_type == "amazon" or format_type == "asin":
|
||||||
return u"https://amazon.com/dp/{0}".format(self.val)
|
return "https://amazon.com/dp/{0}".format(self.val)
|
||||||
elif format_type.startswith('amazon_'):
|
elif format_type.startswith('amazon_'):
|
||||||
return u"https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
|
return "https://amazon.{0}/dp/{1}".format(format_type[7:], self.val)
|
||||||
elif format_type == "isbn":
|
elif format_type == "isbn":
|
||||||
return u"https://www.worldcat.org/isbn/{0}".format(self.val)
|
return "https://www.worldcat.org/isbn/{0}".format(self.val)
|
||||||
elif format_type == "doi":
|
elif format_type == "doi":
|
||||||
return u"https://dx.doi.org/{0}".format(self.val)
|
return "https://dx.doi.org/{0}".format(self.val)
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
return "https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||||
elif format_type == "babelio":
|
elif format_type == "babelio":
|
||||||
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
|
return "https://www.babelio.com/livres/titre/{0}".format(self.val)
|
||||||
elif format_type == "douban":
|
elif format_type == "douban":
|
||||||
return u"https://book.douban.com/subject/{0}".format(self.val)
|
return "https://book.douban.com/subject/{0}".format(self.val)
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
return u"https://books.google.com/books?id={0}".format(self.val)
|
return "https://books.google.com/books?id={0}".format(self.val)
|
||||||
elif format_type == "kobo":
|
elif format_type == "kobo":
|
||||||
return u"https://www.kobo.com/ebook/{0}".format(self.val)
|
return "https://www.kobo.com/ebook/{0}".format(self.val)
|
||||||
elif format_type == "lubimyczytac":
|
elif format_type == "lubimyczytac":
|
||||||
return u"https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
|
return "https://lubimyczytac.pl/ksiazka/{0}/ksiazka".format(self.val)
|
||||||
elif format_type == "litres":
|
elif format_type == "litres":
|
||||||
return u"https://www.litres.ru/{0}".format(self.val)
|
return "https://www.litres.ru/{0}".format(self.val)
|
||||||
elif format_type == "issn":
|
elif format_type == "issn":
|
||||||
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
return "https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||||
elif format_type == "isfdb":
|
elif format_type == "isfdb":
|
||||||
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
return "http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||||
|
elif format_type == "databazeknih":
|
||||||
|
return "https://www.databazeknih.cz/knihy/{0}".format(self.val)
|
||||||
elif self.val.lower().startswith("javascript:"):
|
elif self.val.lower().startswith("javascript:"):
|
||||||
return quote(self.val)
|
return quote(self.val)
|
||||||
|
elif self.val.lower().startswith("data:"):
|
||||||
|
link , __, __ = str.partition(self.val, ",")
|
||||||
|
return link
|
||||||
else:
|
else:
|
||||||
return u"{0}".format(self.val)
|
return "{0}".format(self.val)
|
||||||
|
|
||||||
|
|
||||||
class Comments(Base):
|
class Comments(Base):
|
||||||
@ -188,7 +195,7 @@ class Comments(Base):
|
|||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Comments({0})>".format(self.text)
|
return "<Comments({0})>".format(self.text)
|
||||||
|
|
||||||
|
|
||||||
class Tags(Base):
|
class Tags(Base):
|
||||||
@ -203,8 +210,11 @@ class Tags(Base):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Tags('{0})>".format(self.name)
|
return "<Tags('{0})>".format(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Authors(Base):
|
class Authors(Base):
|
||||||
@ -215,7 +225,7 @@ class Authors(Base):
|
|||||||
sort = Column(String(collation='NOCASE'))
|
sort = Column(String(collation='NOCASE'))
|
||||||
link = Column(String, nullable=False, default="")
|
link = Column(String, nullable=False, default="")
|
||||||
|
|
||||||
def __init__(self, name, sort, link):
|
def __init__(self, name, sort, link=""):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.sort = sort
|
self.sort = sort
|
||||||
self.link = link
|
self.link = link
|
||||||
@ -223,8 +233,11 @@ class Authors(Base):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
return "<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
|
||||||
|
|
||||||
|
|
||||||
class Series(Base):
|
class Series(Base):
|
||||||
@ -241,8 +254,11 @@ class Series(Base):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Series('{0},{1}')>".format(self.name, self.sort)
|
return "<Series('{0},{1}')>".format(self.name, self.sort)
|
||||||
|
|
||||||
|
|
||||||
class Ratings(Base):
|
class Ratings(Base):
|
||||||
@ -257,8 +273,11 @@ class Ratings(Base):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return self.rating
|
return self.rating
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.rating == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Ratings('{0}')>".format(self.rating)
|
return "<Ratings('{0}')>".format(self.rating)
|
||||||
|
|
||||||
|
|
||||||
class Languages(Base):
|
class Languages(Base):
|
||||||
@ -271,13 +290,16 @@ class Languages(Base):
|
|||||||
self.lang_code = lang_code
|
self.lang_code = lang_code
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
if self.language_name:
|
if hasattr(self, "language_name"):
|
||||||
return self.language_name
|
return self.language_name
|
||||||
else:
|
else:
|
||||||
return self.lang_code
|
return self.lang_code
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.lang_code == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Languages('{0}')>".format(self.lang_code)
|
return "<Languages('{0}')>".format(self.lang_code)
|
||||||
|
|
||||||
|
|
||||||
class Publishers(Base):
|
class Publishers(Base):
|
||||||
@ -294,8 +316,11 @@ class Publishers(Base):
|
|||||||
def get(self):
|
def get(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Publishers('{0},{1}')>".format(self.name, self.sort)
|
return "<Publishers('{0},{1}')>".format(self.name, self.sort)
|
||||||
|
|
||||||
|
|
||||||
class Data(Base):
|
class Data(Base):
|
||||||
@ -319,7 +344,7 @@ class Data(Base):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
return "<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
|
||||||
|
|
||||||
|
|
||||||
class Metadata_Dirtied(Base):
|
class Metadata_Dirtied(Base):
|
||||||
@ -373,7 +398,7 @@ class Books(Base):
|
|||||||
self.has_cover = (has_cover != None)
|
self.has_cover = (has_cover != None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
|
return "<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
|
||||||
self.timestamp, self.pubdate, self.series_index,
|
self.timestamp, self.pubdate, self.series_index,
|
||||||
self.last_modified, self.path, self.has_cover)
|
self.last_modified, self.path, self.has_cover)
|
||||||
|
|
||||||
@ -404,7 +429,7 @@ class CustomColumns(Base):
|
|||||||
content['table'] = "custom_column_" + str(self.id)
|
content['table'] = "custom_column_" + str(self.id)
|
||||||
content['column'] = "value"
|
content['column'] = "value"
|
||||||
content['datatype'] = self.datatype
|
content['datatype'] = self.datatype
|
||||||
content['is_multiple'] = None if not self.is_multiple else self.is_multiple
|
content['is_multiple'] = None if not self.is_multiple else "|"
|
||||||
content['kind'] = "field"
|
content['kind'] = "field"
|
||||||
content['name'] = self.name
|
content['name'] = self.name
|
||||||
content['search_terms'] = ['#' + self.label]
|
content['search_terms'] = ['#' + self.label]
|
||||||
@ -418,9 +443,12 @@ class CustomColumns(Base):
|
|||||||
content['is_csp'] = False
|
content['is_csp'] = False
|
||||||
content['is_editable'] = self.editable
|
content['is_editable'] = self.editable
|
||||||
content['rec_index'] = sequence + 22 # toDo why ??
|
content['rec_index'] = sequence + 22 # toDo why ??
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
content['#value#'] = {"__class__": "datetime.datetime", "__value__": value.strftime("%Y-%m-%dT%H:%M:%S+00:00")}
|
||||||
|
else:
|
||||||
content['#value#'] = value
|
content['#value#'] = value
|
||||||
content['#extra#'] = extra
|
content['#extra#'] = extra
|
||||||
content['is_multiple2'] = {}
|
content['is_multiple2'] = {} if not self.is_multiple else {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}
|
||||||
return json.dumps(content, ensure_ascii=False)
|
return json.dumps(content, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
@ -635,7 +663,7 @@ class CalibreDB:
|
|||||||
|
|
||||||
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
||||||
autoflush=True,
|
autoflush=True,
|
||||||
bind=cls.engine))
|
bind=cls.engine, future=True))
|
||||||
for inst in cls.instances:
|
for inst in cls.instances:
|
||||||
inst.init_session()
|
inst.init_session()
|
||||||
|
|
||||||
@ -822,8 +850,6 @@ class CalibreDB:
|
|||||||
|
|
||||||
# Orders all Authors in the list according to authors sort
|
# Orders all Authors in the list according to authors sort
|
||||||
def order_authors(self, entries, list_return=False, combined=False):
|
def order_authors(self, entries, list_return=False, combined=False):
|
||||||
# entries_copy = copy.deepcopy(entries)
|
|
||||||
# entries_copy =[]
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if combined:
|
if combined:
|
||||||
sort_authors = entry.Books.author_sort.split('&')
|
sort_authors = entry.Books.author_sort.split('&')
|
||||||
@ -988,6 +1014,11 @@ class CalibreDB:
|
|||||||
title = title[len(prep):] + ', ' + prep
|
title = title[len(prep):] + ', ' + prep
|
||||||
return title.strip()
|
return title.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# sqlalchemy <1.4.24
|
||||||
|
conn = conn or self.session.connection().connection.driver_connection
|
||||||
|
except AttributeError:
|
||||||
|
# sqlalchemy >1.4.24 and sqlalchemy 2.0
|
||||||
conn = conn or self.session.connection().connection.connection
|
conn = conn or self.session.connection().connection.connection
|
||||||
try:
|
try:
|
||||||
conn.create_function("title_sort", 1, _title_sort)
|
conn.create_function("title_sort", 1, _title_sort)
|
||||||
|
@ -65,7 +65,7 @@ def send_debug():
|
|||||||
file_list.remove(element)
|
file_list.remove(element)
|
||||||
memory_zip = BytesIO()
|
memory_zip = BytesIO()
|
||||||
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
zf.writestr('settings.txt', json.dumps(config.toDict(), sort_keys=True, indent=2))
|
zf.writestr('settings.txt', json.dumps(config.to_dict(), sort_keys=True, indent=2))
|
||||||
zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder))
|
zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder))
|
||||||
for fp in file_list:
|
for fp in file_list:
|
||||||
zf.write(fp, os.path.basename(fp))
|
zf.write(fp, os.path.basename(fp))
|
||||||
|
@ -61,7 +61,7 @@ def dependency_check(optional=False):
|
|||||||
deps = load_dependencies(optional)
|
deps = load_dependencies(optional)
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
try:
|
try:
|
||||||
dep_version_int = [int(x) for x in dep[0].split('.')]
|
dep_version_int = [int(x) if x.isnumeric() else 0 for x in dep[0].split('.')]
|
||||||
low_check = [int(x) for x in dep[3].split('.')]
|
low_check = [int(x) for x in dep[3].split('.')]
|
||||||
high_check = [int(x) for x in dep[5].split('.')]
|
high_check = [int(x) for x in dep[5].split('.')]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
145
cps/editbooks.py
Executable file → Normal file
145
cps/editbooks.py
Executable file → Normal file
@ -25,21 +25,31 @@ from datetime import datetime
|
|||||||
import json
|
import json
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from markupsafe import escape # dependency of flask
|
from markupsafe import escape, Markup # dependency of flask
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
try:
|
||||||
|
from bleach import clean_text as clean_html
|
||||||
|
BLEACH = True
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from nh3 import clean as clean_html
|
||||||
|
BLEACH = False
|
||||||
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
from lxml.html.clean import clean_html
|
from lxml.html.clean import clean_html
|
||||||
|
BLEACH = False
|
||||||
except ImportError:
|
except ImportError:
|
||||||
clean_html = None
|
clean_html = None
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
|
||||||
from sqlalchemy.orm.exc import StaleDataError
|
from sqlalchemy.orm.exc import StaleDataError
|
||||||
|
from sqlalchemy.sql.expression import func
|
||||||
|
|
||||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||||
from . import config, ub, db, calibre_db
|
from . import config, ub, db, calibre_db
|
||||||
@ -107,7 +117,7 @@ def edit_book(book_id):
|
|||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
# Book not found
|
# Book not found
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
|
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
@ -151,7 +161,7 @@ def edit_book(book_id):
|
|||||||
if to_save.get("cover_url", None):
|
if to_save.get("cover_url", None):
|
||||||
if not current_user.role_upload():
|
if not current_user.role_upload():
|
||||||
edit_error = True
|
edit_error = True
|
||||||
flash(_(u"User has no rights to upload cover"), category="error")
|
flash(_("User has no rights to upload cover"), category="error")
|
||||||
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
|
if to_save["cover_url"].endswith('/static/generic_cover.jpg'):
|
||||||
book.has_cover = 0
|
book.has_cover = 0
|
||||||
else:
|
else:
|
||||||
@ -226,7 +236,7 @@ def edit_book(book_id):
|
|||||||
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
|
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error")
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
@ -288,7 +298,7 @@ def upload():
|
|||||||
if error:
|
if error:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
||||||
upload_text = N_(u"File %(file)s uploaded", file=link)
|
upload_text = N_("File %(file)s uploaded", file=link)
|
||||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
|
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
|
||||||
helper.add_book_to_thumbnail_cache(book_id)
|
helper.add_book_to_thumbnail_cache(book_id)
|
||||||
|
|
||||||
@ -302,7 +312,8 @@ def upload():
|
|||||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
@ -315,7 +326,7 @@ def convert_bookformat(book_id):
|
|||||||
book_format_to = request.form.get('book_format_to', None)
|
book_format_to = request.form.get('book_format_to', None)
|
||||||
|
|
||||||
if (book_format_from is None) or (book_format_to is None):
|
if (book_format_from is None) or (book_format_to is None):
|
||||||
flash(_(u"Source or destination format for conversion missing"), category="error")
|
flash(_("Source or destination format for conversion missing"), category="error")
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
|
|
||||||
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
||||||
@ -323,11 +334,11 @@ def convert_bookformat(book_id):
|
|||||||
book_format_to.upper(), current_user.name)
|
book_format_to.upper(), current_user.name)
|
||||||
|
|
||||||
if rtn is None:
|
if rtn is None:
|
||||||
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
flash(_("Book successfully queued for converting to %(book_format)s",
|
||||||
book_format=book_format_to),
|
book_format=book_format_to),
|
||||||
category="success")
|
category="success")
|
||||||
else:
|
else:
|
||||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
flash(_("There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||||
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
@ -451,7 +462,7 @@ def edit_list_book(param):
|
|||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
ret = Response(json.dumps({'success': False,
|
ret = Response(json.dumps({'success': False,
|
||||||
'msg': 'Database error: {}'.format(e.orig)}),
|
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
|
||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -469,7 +480,7 @@ def get_sorted_entry(field, bookid):
|
|||||||
if field == 'sort':
|
if field == 'sort':
|
||||||
return json.dumps({'sort': book.title})
|
return json.dumps({'sort': book.title})
|
||||||
if field == 'author_sort':
|
if field == 'author_sort':
|
||||||
return json.dumps({'author_sort': book.author})
|
return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@ -563,7 +574,7 @@ def table_xchange_author_title():
|
|||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
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: %s", e)
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
return json.dumps({'success': False})
|
return json.dumps({'success': False})
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
@ -573,9 +584,9 @@ def table_xchange_author_title():
|
|||||||
|
|
||||||
|
|
||||||
def merge_metadata(to_save, meta):
|
def merge_metadata(to_save, meta):
|
||||||
if to_save.get('author_name', "") == _(u'Unknown'):
|
if to_save.get('author_name', "") == _('Unknown'):
|
||||||
to_save['author_name'] = ''
|
to_save['author_name'] = ''
|
||||||
if to_save.get('book_title', "") == _(u'Unknown'):
|
if to_save.get('book_title', "") == _('Unknown'):
|
||||||
to_save['book_title'] = ''
|
to_save['book_title'] = ''
|
||||||
for s_field, m_field in [
|
for s_field, m_field in [
|
||||||
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
|
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
|
||||||
@ -597,6 +608,8 @@ def identifier_list(to_save, book):
|
|||||||
val_key = id_val_prefix + type_key[len(id_type_prefix):]
|
val_key = id_val_prefix + type_key[len(id_type_prefix):]
|
||||||
if val_key not in to_save.keys():
|
if val_key not in to_save.keys():
|
||||||
continue
|
continue
|
||||||
|
if to_save[val_key].startswith("data:"):
|
||||||
|
to_save[val_key], __, __ = str.partition(to_save[val_key], ",")
|
||||||
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -611,7 +624,7 @@ def prepare_authors(authr):
|
|||||||
|
|
||||||
# we have all author names now
|
# we have all author names now
|
||||||
if input_authors == ['']:
|
if input_authors == ['']:
|
||||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
input_authors = [_('Unknown')] # prevent empty Author
|
||||||
|
|
||||||
renamed = list()
|
renamed = list()
|
||||||
for in_aut in input_authors:
|
for in_aut in input_authors:
|
||||||
@ -628,11 +641,11 @@ def prepare_authors(authr):
|
|||||||
|
|
||||||
|
|
||||||
def prepare_authors_on_upload(title, authr):
|
def prepare_authors_on_upload(title, authr):
|
||||||
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
if title != _('Unknown') and authr != _('Unknown'):
|
||||||
entry = calibre_db.check_exists_book(authr, title)
|
entry = calibre_db.check_exists_book(authr, title)
|
||||||
if entry:
|
if entry:
|
||||||
log.info("Uploaded book probably exists in library")
|
log.info("Uploaded book probably exists in library")
|
||||||
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
|
flash(_("Uploaded book probably exists in the library, consider to change before upload new: ")
|
||||||
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
|
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
|
||||||
|
|
||||||
input_authors, renamed = prepare_authors(authr)
|
input_authors, renamed = prepare_authors(authr)
|
||||||
@ -687,7 +700,7 @@ def create_book_on_upload(modify_date, meta):
|
|||||||
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
|
modify_date |= edit_book_languages(meta.languages, db_book, upload_mode=True, invalid=invalid)
|
||||||
if invalid:
|
if invalid:
|
||||||
for lang in invalid:
|
for lang in invalid:
|
||||||
flash(_(u"'%(langname)s' is not a valid language", langname=lang), category="warning")
|
flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning")
|
||||||
|
|
||||||
# handle tags
|
# handle tags
|
||||||
modify_date |= edit_book_tags(meta.tags, db_book)
|
modify_date |= edit_book_tags(meta.tags, db_book)
|
||||||
@ -737,7 +750,7 @@ def file_handling_on_upload(requested_file):
|
|||||||
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||||
flash(_(u"File %(filename)s could not saved to temp dir",
|
flash(_("File %(filename)s could not saved to temp dir",
|
||||||
filename=requested_file.filename), category="error")
|
filename=requested_file.filename), category="error")
|
||||||
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
return meta, None
|
return meta, None
|
||||||
@ -757,7 +770,7 @@ def move_coverfile(meta, db_book):
|
|||||||
os.unlink(meta.cover)
|
os.unlink(meta.cover)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.error("Failed to move cover file %s: %s", new_cover_path, e)
|
log.error("Failed to move cover file %s: %s", new_cover_path, e)
|
||||||
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
|
flash(_("Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
|
||||||
error=e),
|
error=e),
|
||||||
category="error")
|
category="error")
|
||||||
|
|
||||||
@ -771,7 +784,7 @@ def delete_whole_book(book_id, book):
|
|||||||
|
|
||||||
# check if only this book links to:
|
# check if only this book links to:
|
||||||
# author, language, series, tags, custom columns
|
# author, language, series, tags, custom columns
|
||||||
modify_database_object([u''], book.authors, db.Authors, calibre_db.session, 'author')
|
modify_database_object([''], book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
|
modify_database_object([u''], book.tags, db.Tags, calibre_db.session, 'tags')
|
||||||
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
|
modify_database_object([u''], book.series, db.Series, calibre_db.session, 'series')
|
||||||
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
@ -892,7 +905,7 @@ def render_edit_book(book_id):
|
|||||||
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
|
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
|
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
@ -927,7 +940,7 @@ def render_edit_book(book_id):
|
|||||||
if kepub_possible:
|
if kepub_possible:
|
||||||
allowed_conversion_formats.append('kepub')
|
allowed_conversion_formats.append('kepub')
|
||||||
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
|
return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
|
||||||
title=_(u"edit metadata"), page="editbook",
|
title=_("edit metadata"), page="editbook",
|
||||||
conversion_formats=allowed_conversion_formats,
|
conversion_formats=allowed_conversion_formats,
|
||||||
config=config,
|
config=config,
|
||||||
source_formats=valid_source_formats)
|
source_formats=valid_source_formats)
|
||||||
@ -988,6 +1001,9 @@ def edit_book_series_index(series_index, book):
|
|||||||
def edit_book_comments(comments, book):
|
def edit_book_comments(comments, book):
|
||||||
modify_date = False
|
modify_date = False
|
||||||
if comments:
|
if comments:
|
||||||
|
if BLEACH:
|
||||||
|
comments = clean_html(comments, tags=None, attributes=None)
|
||||||
|
else:
|
||||||
comments = clean_html(comments)
|
comments = clean_html(comments)
|
||||||
if len(book.comments):
|
if len(book.comments):
|
||||||
if book.comments[0].text != comments:
|
if book.comments[0].text != comments:
|
||||||
@ -1012,7 +1028,7 @@ def edit_book_languages(languages, book, upload_mode=False, invalid=None):
|
|||||||
if isinstance(invalid, list):
|
if isinstance(invalid, list):
|
||||||
invalid.append(lang)
|
invalid.append(lang)
|
||||||
else:
|
else:
|
||||||
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=lang))
|
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
|
||||||
# ToDo: Not working correct
|
# ToDo: Not working correct
|
||||||
if upload_mode and len(input_l) == 1:
|
if upload_mode and len(input_l) == 1:
|
||||||
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
||||||
@ -1123,9 +1139,10 @@ def edit_cc_data(book_id, book, to_save, cc):
|
|||||||
cc_db_value = None
|
cc_db_value = None
|
||||||
if to_save[cc_string].strip():
|
if to_save[cc_string].strip():
|
||||||
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
||||||
changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
||||||
else:
|
else:
|
||||||
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
||||||
|
changed |= change
|
||||||
else:
|
else:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
# remove old cc_val
|
# remove old cc_val
|
||||||
@ -1154,7 +1171,7 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
# check for empty request
|
# check for empty request
|
||||||
if requested_file.filename != '':
|
if requested_file.filename != '':
|
||||||
if not current_user.role_upload():
|
if not current_user.role_upload():
|
||||||
flash(_(u"User has no rights to upload additional file formats"), category="error")
|
flash(_("User has no rights to upload additional file formats"), category="error")
|
||||||
return False
|
return False
|
||||||
if '.' in requested_file.filename:
|
if '.' in requested_file.filename:
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||||
@ -1175,12 +1192,12 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(filepath)
|
os.makedirs(filepath)
|
||||||
except OSError:
|
except OSError:
|
||||||
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
|
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
requested_file.save(saved_filename)
|
requested_file.save(saved_filename)
|
||||||
except OSError:
|
except OSError:
|
||||||
flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error")
|
flash(_("Failed to store file %(file)s.", file=saved_filename), category="error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
file_size = os.path.getsize(saved_filename)
|
file_size = os.path.getsize(saved_filename)
|
||||||
@ -1198,17 +1215,18 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
except (OperationalError, IntegrityError, StaleDataError) as e:
|
except (OperationalError, IntegrityError, StaleDataError) as e:
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
|
||||||
|
category="error")
|
||||||
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
|
||||||
# Queue uploader info
|
# Queue uploader info
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
||||||
upload_text = N_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
upload_text = N_("File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
|
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
|
||||||
|
|
||||||
return uploader.process(
|
return uploader.process(
|
||||||
saved_filename, *os.path.splitext(requested_file.filename),
|
saved_filename, *os.path.splitext(requested_file.filename),
|
||||||
rarExecutable=config.config_rarfile_location)
|
rar_executable=config.config_rarfile_location)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1218,7 +1236,7 @@ def upload_cover(cover_request, book):
|
|||||||
# check for empty request
|
# check for empty request
|
||||||
if requested_file.filename != '':
|
if requested_file.filename != '':
|
||||||
if not current_user.role_upload():
|
if not current_user.role_upload():
|
||||||
flash(_(u"User has no rights to upload cover"), category="error")
|
flash(_("User has no rights to upload cover"), category="error")
|
||||||
return False
|
return False
|
||||||
ret, message = helper.save_cover(requested_file, book.path)
|
ret, message = helper.save_cover(requested_file, book.path)
|
||||||
if ret is True:
|
if ret is True:
|
||||||
@ -1242,18 +1260,18 @@ 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
|
||||||
# handle author(s)
|
# handle author(s)
|
||||||
input_authors, renamed = prepare_authors(author_name)
|
input_authors, renamed = prepare_authors(author_name)
|
||||||
|
|
||||||
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
# change |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
|
|
||||||
# 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
|
||||||
# everything then is assembled for sorted author field in database
|
# everything then is assembled for sorted author field in database
|
||||||
sort_authors_list = list()
|
sort_authors_list = list()
|
||||||
for inp in input_authors:
|
for inp in input_authors:
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
if not stored_author:
|
if not stored_author:
|
||||||
stored_author = helper.get_sorted_author(inp)
|
stored_author = helper.get_sorted_author(inp.replace('|', ','))
|
||||||
else:
|
else:
|
||||||
stored_author = stored_author.sort
|
stored_author = stored_author.sort
|
||||||
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
||||||
@ -1261,6 +1279,9 @@ def handle_author_on_edit(book, author_name, update_stored=True):
|
|||||||
if book.author_sort != sort_authors and update_stored:
|
if book.author_sort != sort_authors and update_stored:
|
||||||
book.author_sort = sort_authors
|
book.author_sort = sort_authors
|
||||||
change = True
|
change = True
|
||||||
|
|
||||||
|
change |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
|
|
||||||
return input_authors, change, renamed
|
return input_authors, change, renamed
|
||||||
|
|
||||||
|
|
||||||
@ -1268,14 +1289,15 @@ def search_objects_remove(db_book_object, db_type, input_elements):
|
|||||||
del_elements = []
|
del_elements = []
|
||||||
for c_elements in db_book_object:
|
for c_elements in db_book_object:
|
||||||
found = False
|
found = False
|
||||||
if db_type == 'languages':
|
#if db_type == 'languages':
|
||||||
type_elements = c_elements.lang_code
|
# type_elements = c_elements.lang_code
|
||||||
elif 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.name
|
||||||
|
type_elements = c_elements
|
||||||
for inp_element in input_elements:
|
for inp_element in input_elements:
|
||||||
if inp_element.lower() == type_elements.lower():
|
if type_elements == inp_element:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
# if the element was not found in the new list, add it to remove list
|
# if the element was not found in the new list, add it to remove list
|
||||||
@ -1289,13 +1311,11 @@ def search_objects_add(db_book_object, db_type, input_elements):
|
|||||||
for inp_element in input_elements:
|
for inp_element in input_elements:
|
||||||
found = False
|
found = False
|
||||||
for c_elements in db_book_object:
|
for c_elements in db_book_object:
|
||||||
if db_type == 'languages':
|
if db_type == 'custom':
|
||||||
type_elements = c_elements.lang_code
|
|
||||||
elif db_type == 'custom':
|
|
||||||
type_elements = c_elements.value
|
type_elements = c_elements.value
|
||||||
else:
|
else:
|
||||||
type_elements = c_elements.name
|
type_elements = c_elements
|
||||||
if inp_element == type_elements:
|
if type_elements == inp_element:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
@ -1311,6 +1331,7 @@ def remove_objects(db_book_object, db_session, del_elements):
|
|||||||
changed = True
|
changed = True
|
||||||
if len(del_element.books) == 0:
|
if len(del_element.books) == 0:
|
||||||
db_session.delete(del_element)
|
db_session.delete(del_element)
|
||||||
|
db_session.flush()
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
@ -1324,10 +1345,14 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
|||||||
db_filter = db_object.name
|
db_filter = db_object.name
|
||||||
for add_element in add_elements:
|
for add_element in add_elements:
|
||||||
# check if an element with that name exists
|
# check if an element with that name exists
|
||||||
db_element = db_session.query(db_object).filter(db_filter == add_element).first()
|
changed = True
|
||||||
|
# db_session.query(db.Tags).filter((func.lower(db.Tags.name).ilike("GênOt"))).all()
|
||||||
|
db_element = db_session.query(db_object).filter((func.lower(db_filter).ilike(add_element))).first()
|
||||||
|
# db_element = db_session.query(db_object).filter(func.lower(db_filter) == add_element.lower()).first()
|
||||||
# if no element is found add it
|
# if no element is found add it
|
||||||
|
if db_element is None:
|
||||||
if db_type == 'author':
|
if db_type == 'author':
|
||||||
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "")
|
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')))
|
||||||
elif db_type == 'series':
|
elif db_type == 'series':
|
||||||
new_element = db_object(add_element, add_element)
|
new_element = db_object(add_element, add_element)
|
||||||
elif db_type == 'custom':
|
elif db_type == 'custom':
|
||||||
@ -1336,15 +1361,18 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
|||||||
new_element = db_object(add_element, None)
|
new_element = db_object(add_element, None)
|
||||||
else: # db_type should be tag or language
|
else: # db_type should be tag or language
|
||||||
new_element = db_object(add_element)
|
new_element = db_object(add_element)
|
||||||
if db_element is None:
|
|
||||||
changed = True
|
|
||||||
db_session.add(new_element)
|
db_session.add(new_element)
|
||||||
db_book_object.append(new_element)
|
db_book_object.append(new_element)
|
||||||
|
else:
|
||||||
|
db_no_case = db_session.query(db_object).filter(db_filter == add_element).first()
|
||||||
|
if db_no_case:
|
||||||
|
# check for new case of element
|
||||||
|
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||||
else:
|
else:
|
||||||
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||||
# add element to book
|
# add element to book
|
||||||
changed = True
|
|
||||||
db_book_object.append(db_element)
|
db_book_object.append(db_element)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
@ -1379,13 +1407,24 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
if not isinstance(input_elements, list):
|
if not isinstance(input_elements, list):
|
||||||
raise TypeError(str(input_elements) + " should be passed as a list")
|
raise TypeError(str(input_elements) + " should be passed as a list")
|
||||||
input_elements = [x for x in input_elements if x != '']
|
input_elements = [x for x in input_elements if x != '']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
# If elements are renamed (upper lower case), rename it
|
||||||
|
for rec_a, rec_b in zip(db_book_object, input_elements):
|
||||||
|
if db_type == "custom":
|
||||||
|
if rec_a.value.casefold() == rec_b.casefold() and rec_a.value != rec_b:
|
||||||
|
create_objects_for_addition(rec_a, rec_b, db_type)
|
||||||
|
else:
|
||||||
|
if rec_a.get().casefold() == rec_b.casefold() and rec_a.get() != rec_b:
|
||||||
|
create_objects_for_addition(rec_a, rec_b, db_type)
|
||||||
# we have all input element (authors, series, tags) names now
|
# we have all input element (authors, series, tags) names now
|
||||||
# 1. search for elements to remove
|
# 1. search for elements to remove
|
||||||
del_elements = search_objects_remove(db_book_object, db_type, input_elements)
|
del_elements = search_objects_remove(db_book_object, db_type, input_elements)
|
||||||
# 2. search for elements that need to be added
|
# 2. search for elements that need to be added
|
||||||
add_elements = search_objects_add(db_book_object, db_type, input_elements)
|
add_elements = search_objects_add(db_book_object, db_type, input_elements)
|
||||||
|
|
||||||
# if there are elements to remove, we remove them now
|
# if there are elements to remove, we remove them now
|
||||||
changed = remove_objects(db_book_object, db_session, del_elements)
|
changed |= remove_objects(db_book_object, db_session, del_elements)
|
||||||
# if there are elements to add, we add them now!
|
# if there are elements to add, we add them now!
|
||||||
if len(add_elements) > 0:
|
if len(add_elements) > 0:
|
||||||
changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements)
|
changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements)
|
||||||
|
54
cps/epub.py
54
cps/epub.py
@ -21,14 +21,16 @@ import zipfile
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from . import isoLanguages, cover
|
from . import isoLanguages, cover
|
||||||
|
from . import config, logger
|
||||||
from .helper import split_authors
|
from .helper import split_authors
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
|
def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
|
||||||
if cover_file is None:
|
if cover_file is None:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
cf = extension = None
|
cf = extension = None
|
||||||
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
|
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
|
||||||
|
|
||||||
@ -41,6 +43,33 @@ def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
|
|||||||
cf = zip_file.read(zip_cover_path)
|
cf = zip_file.read(zip_cover_path)
|
||||||
return cover.cover_processing(tmp_file_name, cf, extension)
|
return cover.cover_processing(tmp_file_name, cf, extension)
|
||||||
|
|
||||||
|
def get_epub_layout(book, book_data):
|
||||||
|
ns = {
|
||||||
|
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
||||||
|
'pkg': 'http://www.idpf.org/2007/opf',
|
||||||
|
}
|
||||||
|
file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower()))
|
||||||
|
|
||||||
|
try:
|
||||||
|
epubZip = zipfile.ZipFile(file_path)
|
||||||
|
txt = epubZip.read('META-INF/container.xml')
|
||||||
|
tree = etree.fromstring(txt)
|
||||||
|
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
||||||
|
cf = epubZip.read(cfname)
|
||||||
|
|
||||||
|
tree = etree.fromstring(cf)
|
||||||
|
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
|
||||||
|
|
||||||
|
layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns)
|
||||||
|
except (etree.XMLSyntaxError, KeyError, IndexError) as e:
|
||||||
|
log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e))
|
||||||
|
layout = []
|
||||||
|
|
||||||
|
if len(layout) == 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
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):
|
||||||
ns = {
|
ns = {
|
||||||
@ -80,13 +109,13 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
if epub_metadata['subject'] == 'Unknown':
|
if epub_metadata['subject'] == 'Unknown':
|
||||||
epub_metadata['subject'] = ''
|
epub_metadata['subject'] = ''
|
||||||
|
|
||||||
if epub_metadata['publisher'] == u'Unknown':
|
if epub_metadata['publisher'] == 'Unknown':
|
||||||
epub_metadata['publisher'] = ''
|
epub_metadata['publisher'] = ''
|
||||||
|
|
||||||
if epub_metadata['date'] == u'Unknown':
|
if epub_metadata['date'] == 'Unknown':
|
||||||
epub_metadata['date'] = ''
|
epub_metadata['date'] = ''
|
||||||
|
|
||||||
if epub_metadata['description'] == u'Unknown':
|
if epub_metadata['description'] == 'Unknown':
|
||||||
description = tree.xpath("//*[local-name() = 'description']/text()")
|
description = tree.xpath("//*[local-name() = 'description']/text()")
|
||||||
if len(description) > 0:
|
if len(description) > 0:
|
||||||
epub_metadata['description'] = description
|
epub_metadata['description'] = description
|
||||||
@ -102,7 +131,10 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
|
|
||||||
identifiers = []
|
identifiers = []
|
||||||
for node in p.xpath('dc:identifier', namespaces=ns):
|
for node in p.xpath('dc:identifier', namespaces=ns):
|
||||||
|
try:
|
||||||
identifier_name = node.attrib.values()[-1]
|
identifier_name = node.attrib.values()[-1]
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
identifier_value = node.text
|
identifier_value = node.text
|
||||||
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
|
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
|
||||||
continue
|
continue
|
||||||
@ -131,13 +163,11 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
|
|
||||||
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
||||||
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
||||||
cover_file = None
|
|
||||||
# if len(cover_section) > 0:
|
|
||||||
for cs in cover_section:
|
for cs in cover_section:
|
||||||
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
|
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
|
||||||
if cover_file:
|
if cover_file:
|
||||||
break
|
return cover_file
|
||||||
if not cover_file:
|
|
||||||
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
|
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
|
||||||
if len(meta_cover) > 0:
|
if len(meta_cover) > 0:
|
||||||
cover_section = tree.xpath(
|
cover_section = tree.xpath(
|
||||||
@ -147,9 +177,10 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
|||||||
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
|
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
|
||||||
else:
|
else:
|
||||||
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
|
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
|
||||||
|
|
||||||
|
cover_file = None
|
||||||
for cs in cover_section:
|
for cs in cover_section:
|
||||||
filetype = cs.rsplit('.', 1)[-1]
|
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
||||||
if filetype == "xhtml" or filetype == "html": # if cover is (x)html format
|
|
||||||
markup = epub_zip.read(os.path.join(cover_path, cs))
|
markup = epub_zip.read(os.path.join(cover_path, cs))
|
||||||
markup_tree = etree.fromstring(markup)
|
markup_tree = etree.fromstring(markup)
|
||||||
# no matter xhtml or html with no namespace
|
# no matter xhtml or html with no namespace
|
||||||
@ -164,7 +195,8 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
|||||||
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
|
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
|
||||||
else:
|
else:
|
||||||
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
|
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
|
||||||
if cover_file: break
|
if cover_file:
|
||||||
|
break
|
||||||
return cover_file
|
return cover_file
|
||||||
|
|
||||||
|
|
||||||
|
14
cps/fb2.py
14
cps/fb2.py
@ -38,19 +38,19 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
if len(last_name):
|
if len(last_name):
|
||||||
last_name = last_name[0]
|
last_name = last_name[0]
|
||||||
else:
|
else:
|
||||||
last_name = u''
|
last_name = ''
|
||||||
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
||||||
if len(middle_name):
|
if len(middle_name):
|
||||||
middle_name = middle_name[0]
|
middle_name = middle_name[0]
|
||||||
else:
|
else:
|
||||||
middle_name = u''
|
middle_name = ''
|
||||||
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
||||||
if len(first_name):
|
if len(first_name):
|
||||||
first_name = first_name[0]
|
first_name = first_name[0]
|
||||||
else:
|
else:
|
||||||
first_name = u''
|
first_name = ''
|
||||||
return (first_name + u' '
|
return (first_name + ' '
|
||||||
+ middle_name + u' '
|
+ middle_name + ' '
|
||||||
+ last_name)
|
+ last_name)
|
||||||
|
|
||||||
author = str(", ".join(map(get_author, authors)))
|
author = str(", ".join(map(get_author, authors)))
|
||||||
@ -59,12 +59,12 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
if len(title):
|
if len(title):
|
||||||
title = str(title[0])
|
title = str(title[0])
|
||||||
else:
|
else:
|
||||||
title = u''
|
title = ''
|
||||||
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
||||||
if len(description):
|
if len(description):
|
||||||
description = str(description[0])
|
description = str(description[0])
|
||||||
else:
|
else:
|
||||||
description = u''
|
description = ''
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
|
@ -55,7 +55,7 @@ def authenticate_google_drive():
|
|||||||
try:
|
try:
|
||||||
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
|
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
|
||||||
except gdriveutils.InvalidConfigError:
|
except gdriveutils.InvalidConfigError:
|
||||||
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
|
flash(_('Google Drive setup not completed, try to deactivate and activate Google Drive again'),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return redirect(authUrl)
|
return redirect(authUrl)
|
||||||
@ -91,9 +91,9 @@ def watch_gdrive():
|
|||||||
config.save()
|
config.save()
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
reason=json.loads(e.content)['error']['errors'][0]
|
reason=json.loads(e.content)['error']['errors'][0]
|
||||||
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
if reason['reason'] == 'push.webhookUrlUnauthorized':
|
||||||
flash(_(u'Callback domain is not verified, '
|
flash(_('Callback domain is not verified, '
|
||||||
u'please follow steps to verify domain in google developer console'), category="error")
|
'please follow steps to verify domain in google developer console'), category="error")
|
||||||
else:
|
else:
|
||||||
flash(reason['message'], category="error")
|
flash(reason['message'], category="error")
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ engine = create_engine('sqlite:///{0}'.format(cli_param.gd_path), echo=False)
|
|||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
# Open session for database connection
|
# Open session for database connection
|
||||||
Session = sessionmaker()
|
Session = sessionmaker(autoflush=False)
|
||||||
Session.configure(bind=engine)
|
Session.configure(bind=engine)
|
||||||
session = scoped_session(Session)
|
session = scoped_session(Session)
|
||||||
|
|
||||||
@ -174,30 +174,12 @@ class PermissionAdded(Base):
|
|||||||
return str(self.gdrive_id)
|
return str(self.gdrive_id)
|
||||||
|
|
||||||
|
|
||||||
def migrate():
|
|
||||||
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
|
|
||||||
PermissionAdded.__table__.create(bind = engine)
|
|
||||||
for sql in session.execute(text("select sql from sqlite_master where type='table'")):
|
|
||||||
if 'CREATE TABLE gdrive_ids' in sql[0]:
|
|
||||||
currUniqueConstraint = 'UNIQUE (gdrive_id)'
|
|
||||||
if currUniqueConstraint in sql[0]:
|
|
||||||
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
|
|
||||||
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
|
|
||||||
session.execute(sql)
|
|
||||||
session.execute("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
|
|
||||||
"gdrive_id, path FROM gdrive_ids;")
|
|
||||||
session.commit()
|
|
||||||
session.execute('DROP TABLE %s' % 'gdrive_ids')
|
|
||||||
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
|
|
||||||
break
|
|
||||||
|
|
||||||
if not os.path.exists(cli_param.gd_path):
|
if not os.path.exists(cli_param.gd_path):
|
||||||
try:
|
try:
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
|
log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
|
||||||
raise
|
raise
|
||||||
migrate()
|
|
||||||
|
|
||||||
|
|
||||||
def getDrive(drive=None, gauth=None):
|
def getDrive(drive=None, gauth=None):
|
||||||
@ -422,7 +404,7 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
|
|||||||
driveFile.Upload()
|
driveFile.Upload()
|
||||||
|
|
||||||
|
|
||||||
def uploadFileToEbooksFolder(destFile, f):
|
def uploadFileToEbooksFolder(destFile, f, string=False):
|
||||||
drive = getDrive(Gdrive.Instance().drive)
|
drive = getDrive(Gdrive.Instance().drive)
|
||||||
parent = getEbooksFolder(drive)
|
parent = getEbooksFolder(drive)
|
||||||
splitDir = destFile.split('/')
|
splitDir = destFile.split('/')
|
||||||
@ -435,7 +417,10 @@ def uploadFileToEbooksFolder(destFile, f):
|
|||||||
else:
|
else:
|
||||||
driveFile = drive.CreateFile({'title': x,
|
driveFile = drive.CreateFile({'title': x,
|
||||||
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
|
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
|
||||||
|
if not string:
|
||||||
driveFile.SetContentFile(f)
|
driveFile.SetContentFile(f)
|
||||||
|
else:
|
||||||
|
driveFile.SetContentString(f)
|
||||||
driveFile.Upload()
|
driveFile.Upload()
|
||||||
else:
|
else:
|
||||||
existing_Folder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
existing_Folder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
|
||||||
@ -556,7 +541,7 @@ def updateGdriveCalibreFromLocal():
|
|||||||
|
|
||||||
# update gdrive.db on edit of books title
|
# update gdrive.db on edit of books title
|
||||||
def updateDatabaseOnEdit(ID,newPath):
|
def updateDatabaseOnEdit(ID,newPath):
|
||||||
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
|
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + '/'
|
||||||
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
||||||
if storedPathName:
|
if storedPathName:
|
||||||
storedPathName.path = sqlCheckPath
|
storedPathName.path = sqlCheckPath
|
||||||
|
195
cps/helper.py
Executable file → Normal file
195
cps/helper.py
Executable file → Normal file
@ -18,6 +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 os
|
import os
|
||||||
|
import random
|
||||||
import io
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
@ -77,29 +78,29 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
|
|||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
data = calibre_db.get_book_format(book.id, old_book_format)
|
data = calibre_db.get_book_format(book.id, old_book_format)
|
||||||
if not data:
|
if not data:
|
||||||
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
error_message = _("%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
|
||||||
log.error("convert_book_format: %s", error_message)
|
log.error("convert_book_format: %s", error_message)
|
||||||
return error_message
|
return error_message
|
||||||
file_path = os.path.join(calibre_path, book.path, data.name)
|
file_path = os.path.join(calibre_path, book.path, data.name)
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
|
if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
|
||||||
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
error_message = _("%(format)s not found on Google Drive: %(fn)s",
|
||||||
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
||||||
return error_message
|
return error_message
|
||||||
else:
|
else:
|
||||||
if not os.path.exists(file_path + "." + old_book_format.lower()):
|
if not os.path.exists(file_path + "." + old_book_format.lower()):
|
||||||
error_message = _(u"%(format)s not found: %(fn)s",
|
error_message = _("%(format)s not found: %(fn)s",
|
||||||
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
format=old_book_format, fn=data.name + "." + old_book_format.lower())
|
||||||
return error_message
|
return error_message
|
||||||
# read settings and append converter task to queue
|
# read settings and append converter task to queue
|
||||||
if ereader_mail:
|
if ereader_mail:
|
||||||
settings = config.get_mail_settings()
|
settings = config.get_mail_settings()
|
||||||
settings['subject'] = _('Send to E-Reader') # pretranslate Subject for e-mail
|
settings['subject'] = _('Send to eReader') # pretranslate Subject for Email
|
||||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
settings['body'] = _('This Email has been sent via Calibre-Web.')
|
||||||
else:
|
else:
|
||||||
settings = dict()
|
settings = dict()
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) # prevent xss
|
||||||
txt = u"{} -> {}: {}".format(
|
txt = "{} -> {}: {}".format(
|
||||||
old_book_format.upper(),
|
old_book_format.upper(),
|
||||||
new_book_format.upper(),
|
new_book_format.upper(),
|
||||||
link)
|
link)
|
||||||
@ -111,30 +112,30 @@ def convert_book_format(book_id, calibre_path, old_book_format, new_book_format,
|
|||||||
|
|
||||||
# Texts are not lazy translated as they are supposed to get send out as is
|
# Texts are not lazy translated as they are supposed to get send out as is
|
||||||
def send_test_mail(ereader_mail, user_name):
|
def send_test_mail(ereader_mail, user_name):
|
||||||
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
WorkerThread.add(user_name, TaskEmail(_('Calibre-Web Test Email'), None, None,
|
||||||
config.get_mail_settings(), ereader_mail, N_(u"Test e-mail"),
|
config.get_mail_settings(), ereader_mail, N_("Test Email"),
|
||||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
_('This Email has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
||||||
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||||
txt = "Hello %s!\r\n" % user_name
|
txt = "Hi %s!\r\n" % user_name
|
||||||
if not resend:
|
if not resend:
|
||||||
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
txt += "Your account at Calibre-Web has been created.\r\n"
|
||||||
txt += "Please log in to your account using the following information:\r\n"
|
txt += "Please log in using the following information:\r\n"
|
||||||
txt += "Username: %s\r\n" % user_name
|
txt += "Username: %s\r\n" % user_name
|
||||||
txt += "Password: %s\r\n" % default_password
|
txt += "Password: %s\r\n" % default_password
|
||||||
txt += "Don't forget to change your password after first login.\r\n"
|
txt += "Don't forget to change your password after your first login.\r\n"
|
||||||
txt += "Sincerely\r\n\r\n"
|
txt += "Regards,\r\n\r\n"
|
||||||
txt += "Your Calibre-Web team"
|
txt += "Calibre-Web"
|
||||||
WorkerThread.add(None, TaskEmail(
|
WorkerThread.add(None, TaskEmail(
|
||||||
subject=_(u'Get Started with Calibre-Web'),
|
subject=_('Get Started with Calibre-Web'),
|
||||||
filepath=None,
|
filepath=None,
|
||||||
attachment=None,
|
attachment=None,
|
||||||
settings=config.get_mail_settings(),
|
settings=config.get_mail_settings(),
|
||||||
recipient=e_mail,
|
recipient=e_mail,
|
||||||
task_message=N_(u"Registration e-mail for user: %(name)s", name=user_name),
|
task_message=N_("Registration Email for user: %(name)s", name=user_name),
|
||||||
text=txt
|
text=txt
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
@ -145,13 +146,13 @@ def check_send_to_ereader_with_converter(formats):
|
|||||||
if 'MOBI' in formats and 'EPUB' not in formats:
|
if 'MOBI' in formats and 'EPUB' not in formats:
|
||||||
book_formats.append({'format': 'Epub',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 1,
|
'convert': 1,
|
||||||
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
|
'text': _('Convert %(orig)s to %(format)s and send to eReader',
|
||||||
orig='Mobi',
|
orig='Mobi',
|
||||||
format='Epub')})
|
format='Epub')})
|
||||||
if 'AZW3' in formats and 'EPUB' not in formats:
|
if 'AZW3' in formats and 'EPUB' not in formats:
|
||||||
book_formats.append({'format': 'Epub',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 2,
|
'convert': 2,
|
||||||
'text': _('Convert %(orig)s to %(format)s and send to E-Reader',
|
'text': _('Convert %(orig)s to %(format)s and send to eReader',
|
||||||
orig='Azw3',
|
orig='Azw3',
|
||||||
format='Epub')})
|
format='Epub')})
|
||||||
return book_formats
|
return book_formats
|
||||||
@ -159,7 +160,7 @@ def check_send_to_ereader_with_converter(formats):
|
|||||||
|
|
||||||
def check_send_to_ereader(entry):
|
def check_send_to_ereader(entry):
|
||||||
"""
|
"""
|
||||||
returns all available book formats for sending to E-Reader
|
returns all available book formats for sending to eReader
|
||||||
"""
|
"""
|
||||||
formats = list()
|
formats = list()
|
||||||
book_formats = list()
|
book_formats = list()
|
||||||
@ -170,31 +171,27 @@ def check_send_to_ereader(entry):
|
|||||||
if 'EPUB' in formats:
|
if 'EPUB' in formats:
|
||||||
book_formats.append({'format': 'Epub',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to E-Reader', format='Epub')})
|
'text': _('Send %(format)s to eReader', format='Epub')})
|
||||||
if 'MOBI' in formats:
|
|
||||||
book_formats.append({'format': 'Mobi',
|
|
||||||
'convert': 0,
|
|
||||||
'text': _('Send %(format)s to E-Reader', format='Mobi')})
|
|
||||||
if 'PDF' in formats:
|
if 'PDF' in formats:
|
||||||
book_formats.append({'format': 'Pdf',
|
book_formats.append({'format': 'Pdf',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to E-Reader', format='Pdf')})
|
'text': _('Send %(format)s to eReader', format='Pdf')})
|
||||||
if 'AZW' in formats:
|
if 'AZW' in formats:
|
||||||
book_formats.append({'format': 'Azw',
|
book_formats.append({'format': 'Azw',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to E-Reader', format='Azw')})
|
'text': _('Send %(format)s to eReader', format='Azw')})
|
||||||
if config.config_converterpath:
|
if config.config_converterpath:
|
||||||
book_formats.extend(check_send_to_ereader_with_converter(formats))
|
book_formats.extend(check_send_to_ereader_with_converter(formats))
|
||||||
return book_formats
|
return book_formats
|
||||||
else:
|
else:
|
||||||
log.error(u'Cannot find book entry %d', entry.id)
|
log.error('Cannot find book entry %d', entry.id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
||||||
# list with supported formats
|
# list with supported formats
|
||||||
def check_read_formats(entry):
|
def check_read_formats(entry):
|
||||||
extensions_reader = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
|
extensions_reader = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU', 'DJV'}
|
||||||
book_formats = list()
|
book_formats = list()
|
||||||
if len(entry.data):
|
if len(entry.data):
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
@ -204,30 +201,30 @@ def check_read_formats(entry):
|
|||||||
|
|
||||||
|
|
||||||
# Files are processed in the following order/priority:
|
# Files are processed in the following order/priority:
|
||||||
# 1: If Mobi file is existing, it's directly send to E-Reader email,
|
# 1: If epub file is existing, it's directly send to eReader email,
|
||||||
# 2: If Epub file is existing, it's converted and send to E-Reader email,
|
# 2: If mobi file is existing, it's converted and send to eReader email,
|
||||||
# 3: If Pdf file is existing, it's directly send to E-Reader email
|
# 3: If Pdf file is existing, it's directly send to eReader email
|
||||||
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
|
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
|
||||||
"""Send email with attachments"""
|
"""Send email with attachments"""
|
||||||
book = calibre_db.get_book(book_id)
|
book = calibre_db.get_book(book_id)
|
||||||
|
|
||||||
if convert == 1:
|
if convert == 1:
|
||||||
# returns None if success, otherwise errormessage
|
# returns None if success, otherwise errormessage
|
||||||
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, ereader_mail)
|
return convert_book_format(book_id, calibrepath, 'mobi', book_format.lower(), user_id, ereader_mail)
|
||||||
if convert == 2:
|
if convert == 2:
|
||||||
# returns None if success, otherwise errormessage
|
# returns None if success, otherwise errormessage
|
||||||
return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, ereader_mail)
|
return convert_book_format(book_id, calibrepath, 'azw3', book_format.lower(), user_id, ereader_mail)
|
||||||
|
|
||||||
for entry in iter(book.data):
|
for entry in iter(book.data):
|
||||||
if entry.format.upper() == book_format.upper():
|
if entry.format.upper() == book_format.upper():
|
||||||
converted_file_name = entry.name + '.' + book_format.lower()
|
converted_file_name = entry.name + '.' + book_format.lower()
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||||
email_text = N_(u"%(book)s send to E-Reader", book=link)
|
email_text = N_("%(book)s send to eReader", book=link)
|
||||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to E-Reader"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), ereader_mail,
|
config.get_mail_settings(), ereader_mail,
|
||||||
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
email_text, _('This Email has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
return _("The requested file could not be read. Maybe wrong permissions?")
|
||||||
|
|
||||||
|
|
||||||
def get_valid_filename(value, replace_whitespace=True, chars=128):
|
def get_valid_filename(value, replace_whitespace=True, chars=128):
|
||||||
@ -235,16 +232,16 @@ def get_valid_filename(value, replace_whitespace=True, chars=128):
|
|||||||
Returns the given string converted to a string that can be used for a clean
|
Returns the given string converted to a string that can be used for a clean
|
||||||
filename. Limits num characters to 128 max.
|
filename. Limits num characters to 128 max.
|
||||||
"""
|
"""
|
||||||
if value[-1:] == u'.':
|
if value[-1:] == '.':
|
||||||
value = value[:-1]+u'_'
|
value = value[:-1]+'_'
|
||||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||||
if config.config_unicode_filename:
|
if config.config_unicode_filename:
|
||||||
value = (unidecode.unidecode(value))
|
value = (unidecode.unidecode(value))
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
# *+:\"/<>? are replaced by _
|
# *+:\"/<>? are replaced by _
|
||||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
value = re.sub(r'[*+:\\\"/<>?]+', '_', value, flags=re.U)
|
||||||
# pipe has to be replaced with comma
|
# pipe has to be replaced with comma
|
||||||
value = re.sub(r'[|]+', u',', value, flags=re.U)
|
value = re.sub(r'[|]+', ',', value, flags=re.U)
|
||||||
|
|
||||||
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
|
value = value.encode('utf-8')[:chars].decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
@ -341,7 +338,7 @@ def edit_book_read_status(book_id, read_status=None):
|
|||||||
return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column)
|
return "Custom Column No.{} does not exist in calibre database".format(config.config_read_column)
|
||||||
except (OperationalError, InvalidRequestError) as ex:
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error(u"Read status could not set: {}".format(ex))
|
log.error("Read status could not set: {}".format(ex))
|
||||||
return _("Read status could not set: {}".format(ex.orig))
|
return _("Read status could not set: {}".format(ex.orig))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -416,8 +413,8 @@ def clean_author_database(renamed_author, calibre_path="", local_book=None, gdri
|
|||||||
g_file = gd.getFileFromEbooksFolder(all_new_path,
|
g_file = gd.getFileFromEbooksFolder(all_new_path,
|
||||||
file_format.name + '.' + file_format.format.lower())
|
file_format.name + '.' + file_format.format.lower())
|
||||||
if g_file:
|
if g_file:
|
||||||
gd.moveGdriveFileRemote(g_file, all_new_name + u'.' + file_format.format.lower())
|
gd.moveGdriveFileRemote(g_file, all_new_name + '.' + file_format.format.lower())
|
||||||
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + u'.' + file_format.format.lower())
|
gd.updateDatabaseOnEdit(g_file['id'], all_new_name + '.' + file_format.format.lower())
|
||||||
else:
|
else:
|
||||||
log.error("File {} not found on gdrive"
|
log.error("File {} not found on gdrive"
|
||||||
.format(all_new_path, file_format.name + '.' + file_format.format.lower()))
|
.format(all_new_path, file_format.name + '.' + file_format.format.lower()))
|
||||||
@ -510,25 +507,25 @@ def update_dir_structure_gdrive(book_id, first_author, renamed_author):
|
|||||||
authordir = book.path.split('/')[0]
|
authordir = book.path.split('/')[0]
|
||||||
titledir = book.path.split('/')[1]
|
titledir = book.path.split('/')[1]
|
||||||
new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True)
|
new_authordir = rename_all_authors(first_author, renamed_author, gdrive=True)
|
||||||
new_titledir = get_valid_filename(book.title, chars=96) + u" (" + str(book_id) + u")"
|
new_titledir = get_valid_filename(book.title, chars=96) + " (" + str(book_id) + ")"
|
||||||
|
|
||||||
if titledir != new_titledir:
|
if titledir != new_titledir:
|
||||||
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||||
if g_file:
|
if g_file:
|
||||||
gd.moveGdriveFileRemote(g_file, new_titledir)
|
gd.moveGdriveFileRemote(g_file, new_titledir)
|
||||||
book.path = book.path.split('/')[0] + u'/' + new_titledir
|
book.path = book.path.split('/')[0] + '/' + new_titledir
|
||||||
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
|
gd.updateDatabaseOnEdit(g_file['id'], book.path) # only child folder affected
|
||||||
else:
|
else:
|
||||||
return _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
|
return _('File %(file)s not found on Google Drive', file=book.path) # file not found
|
||||||
|
|
||||||
if authordir != new_authordir and authordir not in renamed_author:
|
if authordir != new_authordir and authordir not in renamed_author:
|
||||||
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
|
g_file = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
|
||||||
if g_file:
|
if g_file:
|
||||||
gd.moveGdriveFolderRemote(g_file, new_authordir)
|
gd.moveGdriveFolderRemote(g_file, new_authordir)
|
||||||
book.path = new_authordir + u'/' + book.path.split('/')[1]
|
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||||
gd.updateDatabaseOnEdit(g_file['id'], book.path)
|
gd.updateDatabaseOnEdit(g_file['id'], book.path)
|
||||||
else:
|
else:
|
||||||
return _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
|
return _('File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||||
|
|
||||||
# change location in database to new author/title path
|
# change location in database to new author/title path
|
||||||
book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
|
book.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
|
||||||
@ -600,7 +597,7 @@ def delete_book_gdrive(book, book_format):
|
|||||||
gd.deleteDatabaseEntry(g_file['id'])
|
gd.deleteDatabaseEntry(g_file['id'])
|
||||||
g_file.Trash()
|
g_file.Trash()
|
||||||
else:
|
else:
|
||||||
error = _(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
error = _('Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||||
|
|
||||||
return error is None, error
|
return error is None, error
|
||||||
|
|
||||||
@ -612,7 +609,7 @@ def reset_password(user_id):
|
|||||||
if not config.get_mail_server_configured():
|
if not config.get_mail_server_configured():
|
||||||
return 2, None
|
return 2, None
|
||||||
try:
|
try:
|
||||||
password = generate_random_password()
|
password = generate_random_password(config.config_password_min_length)
|
||||||
existing_user.password = generate_password_hash(password)
|
existing_user.password = generate_password_hash(password)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
send_registration_mail(existing_user.email, existing_user.name, password, True)
|
send_registration_mail(existing_user.email, existing_user.name, password, True)
|
||||||
@ -621,11 +618,35 @@ def reset_password(user_id):
|
|||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
return 0, None
|
return 0, None
|
||||||
|
|
||||||
|
def generate_random_password(min_length):
|
||||||
|
min_length = max(8, min_length) - 4
|
||||||
|
random_source = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||||
|
# select 1 lowercase
|
||||||
|
s = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
password = [s[c % len(s)] for c in os.urandom(1)]
|
||||||
|
# select 1 uppercase
|
||||||
|
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
password.extend([s[c % len(s)] for c in os.urandom(1)])
|
||||||
|
# select 1 digit
|
||||||
|
s = "01234567890"
|
||||||
|
password.extend([s[c % len(s)] for c in os.urandom(1)])
|
||||||
|
# select 1 special symbol
|
||||||
|
s = "!@#$%&*()?"
|
||||||
|
password.extend([s[c % len(s)] for c in os.urandom(1)])
|
||||||
|
|
||||||
def generate_random_password():
|
# generate other characters
|
||||||
|
password.extend([random_source[c % len(random_source)] for c in os.urandom(min_length)])
|
||||||
|
|
||||||
|
# password_list = list(password)
|
||||||
|
# shuffle all characters
|
||||||
|
random.SystemRandom().shuffle(password)
|
||||||
|
return ''.join(password)
|
||||||
|
|
||||||
|
|
||||||
|
'''def generate_random_password(min_length):
|
||||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||||
passlen = 8
|
passlen = min_length
|
||||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
return "".join(s[c % len(s)] for c in os.urandom(passlen))'''
|
||||||
|
|
||||||
|
|
||||||
def uniq(inpt):
|
def uniq(inpt):
|
||||||
@ -640,16 +661,16 @@ def uniq(inpt):
|
|||||||
def check_email(email):
|
def check_email(email):
|
||||||
email = valid_email(email)
|
email = valid_email(email)
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
|
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
|
||||||
log.error(u"Found an existing account for this e-mail address")
|
log.error("Found an existing account for this Email address")
|
||||||
raise Exception(_(u"Found an existing account for this e-mail address"))
|
raise Exception(_("Found an existing account for this Email address"))
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
|
||||||
def check_username(username):
|
def check_username(username):
|
||||||
username = username.strip()
|
username = username.strip()
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
|
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
|
||||||
log.error(u"This username is already taken")
|
log.error("This username is already taken")
|
||||||
raise Exception(_(u"This username is already taken"))
|
raise Exception(_("This username is already taken"))
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
|
||||||
@ -660,10 +681,27 @@ def valid_email(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(u"Invalid e-mail address format")
|
log.error("Invalid Email address format")
|
||||||
raise Exception(_(u"Invalid e-mail address format"))
|
raise Exception(_("Invalid Email address format"))
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
def valid_password(check_password):
|
||||||
|
if config.config_password_policy:
|
||||||
|
verify = ""
|
||||||
|
if config.config_password_min_length > 0:
|
||||||
|
verify += "^(?=.{" + str(config.config_password_min_length) + ",}$)"
|
||||||
|
if config.config_password_number:
|
||||||
|
verify += "(?=.*?\d)"
|
||||||
|
if config.config_password_lower:
|
||||||
|
verify += "(?=.*?[a-z])"
|
||||||
|
if config.config_password_upper:
|
||||||
|
verify += "(?=.*?[A-Z])"
|
||||||
|
if config.config_password_special:
|
||||||
|
verify += "(?=.*?[^A-Za-z\s0-9])"
|
||||||
|
match = re.match(verify, check_password)
|
||||||
|
if not match:
|
||||||
|
raise Exception(_("Password doesn't comply with password validation rules"))
|
||||||
|
return check_password
|
||||||
# ################################# External interface #################################
|
# ################################# External interface #################################
|
||||||
|
|
||||||
|
|
||||||
@ -694,28 +732,27 @@ def delete_book(book, calibrepath, book_format):
|
|||||||
return delete_book_file(book, calibrepath, book_format)
|
return delete_book_file(book, calibrepath, book_format)
|
||||||
|
|
||||||
|
|
||||||
def get_cover_on_failure(use_generic_cover):
|
def get_cover_on_failure():
|
||||||
if use_generic_cover:
|
|
||||||
try:
|
try:
|
||||||
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
log.error("No permission to access generic_cover.jpg file.")
|
log.error("No permission to access generic_cover.jpg file.")
|
||||||
abort(403)
|
abort(403)
|
||||||
abort(404)
|
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover(book_id, resolution=None):
|
def get_book_cover(book_id, resolution=None):
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
|
return get_book_cover_internal(book, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
# Called only by kobo sync -> cover not found should be answered with 404 and not with default cover
|
|
||||||
def get_book_cover_with_uuid(book_uuid, resolution=None):
|
def get_book_cover_with_uuid(book_uuid, resolution=None):
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
return get_book_cover_internal(book, use_generic_cover_on_failure=False, resolution=resolution)
|
if not book:
|
||||||
|
return # allows kobo.HandleCoverImageRequest to proxy request
|
||||||
|
return get_book_cover_internal(book, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None):
|
def get_book_cover_internal(book, resolution=None):
|
||||||
if book and book.has_cover:
|
if book and book.has_cover:
|
||||||
|
|
||||||
# Send the book cover thumbnail if it exists in cache
|
# Send the book cover thumbnail if it exists in cache
|
||||||
@ -731,16 +768,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
|
|||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
try:
|
try:
|
||||||
if not gd.is_gdrive_ready():
|
if not gd.is_gdrive_ready():
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure()
|
||||||
path = gd.get_cover_via_gdrive(book.path)
|
path = gd.get_cover_via_gdrive(book.path)
|
||||||
if path:
|
if path:
|
||||||
return redirect(path)
|
return redirect(path)
|
||||||
else:
|
else:
|
||||||
log.error('{}/cover.jpg not found on Google Drive'.format(book.path))
|
log.error('{}/cover.jpg not found on Google Drive'.format(book.path))
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure()
|
||||||
|
|
||||||
# Send the book cover from the Calibre directory
|
# Send the book cover from the Calibre directory
|
||||||
else:
|
else:
|
||||||
@ -748,9 +785,9 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
|
|||||||
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
||||||
return send_from_directory(cover_file_path, "cover.jpg")
|
return send_from_directory(cover_file_path, "cover.jpg")
|
||||||
else:
|
else:
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure()
|
||||||
else:
|
else:
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure()
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover_thumbnail(book, resolution):
|
def get_book_cover_thumbnail(book, resolution):
|
||||||
@ -773,7 +810,7 @@ def get_series_thumbnail_on_failure(series_id, resolution):
|
|||||||
.filter(db.Books.has_cover == 1) \
|
.filter(db.Books.has_cover == 1) \
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
|
return get_book_cover_internal(book, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
def get_series_cover_thumbnail(series_id, resolution=None):
|
def get_series_cover_thumbnail(series_id, resolution=None):
|
||||||
@ -837,8 +874,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(filepath)
|
os.makedirs(filepath)
|
||||||
except OSError:
|
except OSError:
|
||||||
log.error(u"Failed to create path for cover")
|
log.error("Failed to create path for cover")
|
||||||
return False, _(u"Failed to create path for cover")
|
return False, _("Failed to create path for cover")
|
||||||
try:
|
try:
|
||||||
# upload of jgp file without wand
|
# upload of jgp file without wand
|
||||||
if isinstance(img, requests.Response):
|
if isinstance(img, requests.Response):
|
||||||
@ -853,8 +890,8 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
# upload of jpg/png... from hdd
|
# upload of jpg/png... from hdd
|
||||||
img.save(os.path.join(filepath, saved_filename))
|
img.save(os.path.join(filepath, saved_filename))
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
log.error("Cover-file is not a valid image file, or could not be stored")
|
||||||
return False, _(u"Cover-file is not a valid image file, or could not be stored")
|
return False, _("Cover-file is not a valid image file, or could not be stored")
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
@ -1004,7 +1041,7 @@ def get_download_link(book_id, book_format, client):
|
|||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||||
quote(file_name.encode('utf-8')), book_format, quote(file_name.encode('utf-8')), book_format)
|
quote(file_name), book_format, quote(file_name), book_format)
|
||||||
return do_download_file(book, book_format, client, data1, headers)
|
return do_download_file(book, book_format, client, data1, headers)
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
File diff suppressed because it is too large
Load Diff
98
cps/kobo.py
98
cps/kobo.py
@ -21,6 +21,7 @@ import base64
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
import zipfile
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
import json
|
import json
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@ -46,7 +47,8 @@ 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 .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
|
from .epub import get_epub_layout
|
||||||
|
from .constants import COVER_THUMBNAIL_SMALL #, sqlalchemy_version2
|
||||||
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
|
||||||
@ -54,7 +56,7 @@ from .kobo_auth import requires_kobo_auth, get_auth_token
|
|||||||
|
|
||||||
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||||
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
|
KOBO_IMAGEHOST_URL = "https://cdn.kobo.com/book-images"
|
||||||
|
|
||||||
SYNC_ITEM_LIMIT = 100
|
SYNC_ITEM_LIMIT = 100
|
||||||
|
|
||||||
@ -140,6 +142,7 @@ def HandleSyncRequest():
|
|||||||
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
sync_token = SyncToken.SyncToken.from_headers(request.headers)
|
||||||
log.info("Kobo library sync request received.")
|
log.info("Kobo library sync request received.")
|
||||||
log.debug("SyncToken: {}".format(sync_token))
|
log.debug("SyncToken: {}".format(sync_token))
|
||||||
|
log.debug("Download link format {}".format(get_download_url_for_book('[bookid]','[bookformat]')))
|
||||||
if not current_app.wsgi_app.is_proxied:
|
if not current_app.wsgi_app.is_proxied:
|
||||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||||
|
|
||||||
@ -163,12 +166,6 @@ def HandleSyncRequest():
|
|||||||
only_kobo_shelves = current_user.kobo_only_shelves_sync
|
only_kobo_shelves = current_user.kobo_only_shelves_sync
|
||||||
|
|
||||||
if only_kobo_shelves:
|
if only_kobo_shelves:
|
||||||
if sqlalchemy_version2:
|
|
||||||
changed_entries = select(db.Books,
|
|
||||||
ub.ArchivedBook.last_modified,
|
|
||||||
ub.BookShelf.date_added,
|
|
||||||
ub.ArchivedBook.is_archived)
|
|
||||||
else:
|
|
||||||
changed_entries = calibre_db.session.query(db.Books,
|
changed_entries = calibre_db.session.query(db.Books,
|
||||||
ub.ArchivedBook.last_modified,
|
ub.ArchivedBook.last_modified,
|
||||||
ub.BookShelf.date_added,
|
ub.BookShelf.date_added,
|
||||||
@ -188,9 +185,6 @@ def HandleSyncRequest():
|
|||||||
.filter(ub.Shelf.user_id == current_user.id)
|
.filter(ub.Shelf.user_id == current_user.id)
|
||||||
.filter(ub.Shelf.kobo_sync)
|
.filter(ub.Shelf.kobo_sync)
|
||||||
.distinct())
|
.distinct())
|
||||||
else:
|
|
||||||
if sqlalchemy_version2:
|
|
||||||
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
|
||||||
else:
|
else:
|
||||||
changed_entries = calibre_db.session.query(db.Books,
|
changed_entries = calibre_db.session.query(db.Books,
|
||||||
ub.ArchivedBook.last_modified,
|
ub.ArchivedBook.last_modified,
|
||||||
@ -206,9 +200,6 @@ def HandleSyncRequest():
|
|||||||
.order_by(db.Books.id))
|
.order_by(db.Books.id))
|
||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
if sqlalchemy_version2:
|
|
||||||
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
|
||||||
else:
|
|
||||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||||
log.debug("Books to Sync: {}".format(len(books.all())))
|
log.debug("Books to Sync: {}".format(len(books.all())))
|
||||||
for book in books:
|
for book in books:
|
||||||
@ -227,7 +218,7 @@ def HandleSyncRequest():
|
|||||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||||
reading_states_in_new_entitlements.append(book.Books.id)
|
reading_states_in_new_entitlements.append(book.Books.id)
|
||||||
|
|
||||||
ts_created = book.Books.timestamp
|
ts_created = book.Books.timestamp.replace(tzinfo=None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ts_created = max(ts_created, book.date_added)
|
ts_created = max(ts_created, book.date_added)
|
||||||
@ -240,7 +231,7 @@ def HandleSyncRequest():
|
|||||||
sync_results.append({"ChangedEntitlement": entitlement})
|
sync_results.append({"ChangedEntitlement": entitlement})
|
||||||
|
|
||||||
new_books_last_modified = max(
|
new_books_last_modified = max(
|
||||||
book.Books.last_modified, new_books_last_modified
|
book.Books.last_modified.replace(tzinfo=None), new_books_last_modified
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
new_books_last_modified = max(
|
new_books_last_modified = max(
|
||||||
@ -252,14 +243,7 @@ def HandleSyncRequest():
|
|||||||
new_books_last_created = max(ts_created, new_books_last_created)
|
new_books_last_created = max(ts_created, new_books_last_created)
|
||||||
kobo_sync_status.add_synced_books(book.Books.id)
|
kobo_sync_status.add_synced_books(book.Books.id)
|
||||||
|
|
||||||
if sqlalchemy_version2:
|
max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\
|
||||||
max_change = calibre_db.session.execute(changed_entries
|
|
||||||
.filter(ub.ArchivedBook.is_archived)
|
|
||||||
.filter(ub.ArchivedBook.user_id == current_user.id)
|
|
||||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
|
||||||
.columns(db.Books).first()
|
|
||||||
else:
|
|
||||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
|
||||||
.filter(ub.ArchivedBook.user_id == current_user.id) \
|
.filter(ub.ArchivedBook.user_id == current_user.id) \
|
||||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||||
|
|
||||||
@ -268,10 +252,6 @@ def HandleSyncRequest():
|
|||||||
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||||
|
|
||||||
# no. of books returned
|
# no. of books returned
|
||||||
if sqlalchemy_version2:
|
|
||||||
entries = calibre_db.session.execute(changed_entries).all()
|
|
||||||
book_count = len(entries)
|
|
||||||
else:
|
|
||||||
book_count = changed_entries.count()
|
book_count = changed_entries.count()
|
||||||
# last entry:
|
# last entry:
|
||||||
cont_sync = bool(book_count)
|
cont_sync = bool(book_count)
|
||||||
@ -335,7 +315,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
|||||||
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
|
log.error_or_exception("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
|
||||||
if set_cont:
|
if set_cont:
|
||||||
extra_headers["x-kobo-sync"] = "continue"
|
extra_headers["x-kobo-sync"] = "continue"
|
||||||
sync_token.to_headers(extra_headers)
|
sync_token.to_headers(extra_headers)
|
||||||
@ -356,7 +336,7 @@ def HandleMetadataRequest(book_uuid):
|
|||||||
log.info("Kobo library metadata request received for book %s" % book_uuid)
|
log.info("Kobo library metadata request received for book %s" % book_uuid)
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
if not book or not book.data:
|
if not book or not book.data:
|
||||||
log.info(u"Book %s not found in database", book_uuid)
|
log.info("Book %s not found in database", book_uuid)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
metadata = get_metadata(book)
|
metadata = get_metadata(book)
|
||||||
@ -365,7 +345,7 @@ def HandleMetadataRequest(book_uuid):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_download_url_for_book(book, book_format):
|
def get_download_url_for_book(book_id, book_format):
|
||||||
if not current_app.wsgi_app.is_proxied:
|
if not current_app.wsgi_app.is_proxied:
|
||||||
if ':' in request.host and not request.host.endswith(']'):
|
if ':' in request.host and not request.host.endswith(']'):
|
||||||
host = "".join(request.host.split(':')[:-1])
|
host = "".join(request.host.split(':')[:-1])
|
||||||
@ -377,13 +357,13 @@ def get_download_url_for_book(book, book_format):
|
|||||||
url_base=host,
|
url_base=host,
|
||||||
url_port=config.config_external_port,
|
url_port=config.config_external_port,
|
||||||
auth_token=get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_id=book.id,
|
book_id=book_id,
|
||||||
book_format=book_format.lower()
|
book_format=book_format.lower()
|
||||||
)
|
)
|
||||||
return url_for(
|
return url_for(
|
||||||
"kobo.download_book",
|
"kobo.download_book",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=kobo_auth.get_auth_token(),
|
||||||
book_id=book.id,
|
book_id=book_id,
|
||||||
book_format=book_format.lower(),
|
book_format=book_format.lower(),
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
)
|
||||||
@ -459,16 +439,21 @@ def get_metadata(book):
|
|||||||
continue
|
continue
|
||||||
for kobo_format in KOBO_FORMATS[book_data.format]:
|
for kobo_format in KOBO_FORMATS[book_data.format]:
|
||||||
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
|
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
|
||||||
|
try:
|
||||||
|
if get_epub_layout(book, book_data) == 'pre-paginated':
|
||||||
|
kobo_format = 'EPUB3FL'
|
||||||
download_urls.append(
|
download_urls.append(
|
||||||
{
|
{
|
||||||
"Format": kobo_format,
|
"Format": kobo_format,
|
||||||
"Size": book_data.uncompressed_size,
|
"Size": book_data.uncompressed_size,
|
||||||
"Url": get_download_url_for_book(book, book_data.format),
|
"Url": get_download_url_for_book(book.id, book_data.format),
|
||||||
# The Kobo forma accepts platforms: (Generic, Android)
|
# The Kobo forma accepts platforms: (Generic, Android)
|
||||||
"Platform": "Generic",
|
"Platform": "Generic",
|
||||||
# "DrmType": "None", # Not required
|
# "DrmType": "None", # Not required
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
except (zipfile.BadZipfile, FileNotFoundError) as e:
|
||||||
|
log.error(e)
|
||||||
|
|
||||||
book_uuid = book.uuid
|
book_uuid = book.uuid
|
||||||
metadata = {
|
metadata = {
|
||||||
@ -515,7 +500,7 @@ def get_metadata(book):
|
|||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
def HandleTagCreate():
|
def HandleTagCreate():
|
||||||
# catch delete requests, otherwise the are handled in the book delete handler
|
# catch delete requests, otherwise they are handled in the book delete handler
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
abort(405)
|
abort(405)
|
||||||
name, items = None, None
|
name, items = None, None
|
||||||
@ -709,14 +694,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
|||||||
})
|
})
|
||||||
extra_filters.append(ub.Shelf.kobo_sync)
|
extra_filters.append(ub.Shelf.kobo_sync)
|
||||||
|
|
||||||
if sqlalchemy_version2:
|
|
||||||
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
|
||||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
|
||||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
|
||||||
ub.Shelf.user_id == current_user.id,
|
|
||||||
*extra_filters
|
|
||||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
|
||||||
else:
|
|
||||||
shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
||||||
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
|
||||||
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
|
||||||
@ -759,7 +736,7 @@ def create_kobo_tag(shelf):
|
|||||||
for book_shelf in shelf.books:
|
for book_shelf in shelf.books:
|
||||||
book = calibre_db.get_book(book_shelf.book_id)
|
book = calibre_db.get_book(book_shelf.book_id)
|
||||||
if not book:
|
if not book:
|
||||||
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
log.info("Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
||||||
continue
|
continue
|
||||||
tag["Items"].append(
|
tag["Items"].append(
|
||||||
{
|
{
|
||||||
@ -776,7 +753,7 @@ def create_kobo_tag(shelf):
|
|||||||
def HandleStateRequest(book_uuid):
|
def HandleStateRequest(book_uuid):
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
if not book or not book.data:
|
if not book or not book.data:
|
||||||
log.info(u"Book %s not found in database", book_uuid)
|
log.info("Book %s not found in database", book_uuid)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
kobo_reading_state = get_or_create_reading_state(book.id)
|
kobo_reading_state = get_or_create_reading_state(book.id)
|
||||||
@ -923,20 +900,26 @@ def get_current_bookmark_response(current_bookmark):
|
|||||||
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
|
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
||||||
book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=COVER_THUMBNAIL_SMALL)
|
try:
|
||||||
if not book_cover:
|
resolution = None if int(height) > 1000 else COVER_THUMBNAIL_SMALL
|
||||||
if config.config_kobo_proxy:
|
except ValueError:
|
||||||
log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid)
|
log.error("Requested height %s of book %s is invalid" % (book_uuid, height))
|
||||||
|
resolution = COVER_THUMBNAIL_SMALL
|
||||||
|
book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=resolution)
|
||||||
|
if book_cover:
|
||||||
|
log.debug("Serving local cover image of book %s" % book_uuid)
|
||||||
|
return book_cover
|
||||||
|
|
||||||
|
if not config.config_kobo_proxy:
|
||||||
|
log.debug("Returning 404 for cover image of unknown book %s" % book_uuid)
|
||||||
|
# additional proxy request make no sense, -> direct return
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
log.debug("Redirecting request for cover image of unknown book %s to Kobo" % book_uuid)
|
||||||
return redirect(KOBO_IMAGEHOST_URL +
|
return redirect(KOBO_IMAGEHOST_URL +
|
||||||
"/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid,
|
"/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid,
|
||||||
width=width,
|
width=width,
|
||||||
height=height), 307)
|
height=height), 307)
|
||||||
else:
|
|
||||||
log.debug("Cover for unknown book: %s requested" % book_uuid)
|
|
||||||
# additional proxy request make no sense, -> direct return
|
|
||||||
return make_response(jsonify({}))
|
|
||||||
log.debug("Cover request received for book %s" % book_uuid)
|
|
||||||
return book_cover
|
|
||||||
|
|
||||||
|
|
||||||
@kobo.route("")
|
@kobo.route("")
|
||||||
@ -951,7 +934,7 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
log.info("Kobo book delete request received for book %s" % book_uuid)
|
log.info("Kobo book delete request received for book %s" % book_uuid)
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
if not book:
|
if not book:
|
||||||
log.info(u"Book %s not found in database", book_uuid)
|
log.info("Book %s not found in database", book_uuid)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
book_id = book.id
|
book_id = book.id
|
||||||
@ -976,6 +959,7 @@ def HandleUnimplementedRequest(dummy=None):
|
|||||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
@kobo.route("/v1/user/recommendations", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/assets", methods=["GET"])
|
||||||
def HandleUserRequest(dummy=None):
|
def HandleUserRequest(dummy=None):
|
||||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
@ -1034,7 +1018,7 @@ def make_calibre_web_auth_response():
|
|||||||
"RefreshToken": RefreshToken,
|
"RefreshToken": RefreshToken,
|
||||||
"TokenType": "Bearer",
|
"TokenType": "Bearer",
|
||||||
"TrackingId": str(uuid.uuid4()),
|
"TrackingId": str(uuid.uuid4()),
|
||||||
"UserKey": content['UserKey'],
|
"UserKey": content.get('UserKey',""),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -64,11 +64,12 @@ from datetime import datetime
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import g, Blueprint, url_for, abort, request
|
from flask import g, Blueprint, abort, request
|
||||||
from flask_login import login_user, current_user, login_required
|
from flask_login import login_user, current_user, login_required
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask_limiter import RateLimitExceeded
|
||||||
|
|
||||||
from . import logger, config, calibre_db, db, helper, ub, lm
|
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@ -112,7 +113,7 @@ def generate_auth_token(user_id):
|
|||||||
|
|
||||||
return render_title_template(
|
return render_title_template(
|
||||||
"generate_kobo_auth_url.html",
|
"generate_kobo_auth_url.html",
|
||||||
title=_(u"Kobo Setup"),
|
title=_("Kobo Setup"),
|
||||||
auth_token=auth_token.auth_token,
|
auth_token=auth_token.auth_token,
|
||||||
warning = warning
|
warning = warning
|
||||||
)
|
)
|
||||||
@ -151,6 +152,10 @@ def requires_kobo_auth(f):
|
|||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
auth_token = get_auth_token()
|
auth_token = get_auth_token()
|
||||||
if auth_token is not None:
|
if auth_token is not None:
|
||||||
|
try:
|
||||||
|
limiter.check()
|
||||||
|
except RateLimitExceeded:
|
||||||
|
return abort(429)
|
||||||
user = (
|
user = (
|
||||||
ub.session.query(ub.User)
|
ub.session.query(ub.User)
|
||||||
.join(ub.RemoteAuthToken)
|
.join(ub.RemoteAuthToken)
|
||||||
@ -159,6 +164,7 @@ def requires_kobo_auth(f):
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
log.debug("Received Kobo request without a recognizable auth token.")
|
log.debug("Received Kobo request without a recognizable auth token.")
|
||||||
return abort(401)
|
return abort(401)
|
||||||
|
@ -150,7 +150,7 @@ def setup(log_file, log_level=None):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
except IOError:
|
except (IOError, PermissionError):
|
||||||
if log_file == DEFAULT_LOG_FILE:
|
if log_file == DEFAULT_LOG_FILE:
|
||||||
raise
|
raise
|
||||||
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
@ -177,7 +177,7 @@ def create_access_log(log_file, log_name, formatter):
|
|||||||
access_log.setLevel(logging.INFO)
|
access_log.setLevel(logging.INFO)
|
||||||
try:
|
try:
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
|
||||||
except IOError:
|
except (IOError, PermissionError):
|
||||||
if log_file == DEFAULT_ACCESS_LOG:
|
if log_file == DEFAULT_ACCESS_LOG:
|
||||||
raise
|
raise
|
||||||
file_handler = RotatingFileHandler(DEFAULT_ACCESS_LOG, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(DEFAULT_ACCESS_LOG, maxBytes=50000, backupCount=2, encoding='utf-8')
|
||||||
|
10
cps/main.py
10
cps/main.py
@ -18,9 +18,14 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from . import create_app
|
from . import create_app, limiter
|
||||||
from .jinjia import jinjia
|
from .jinjia import jinjia
|
||||||
from .remotelogin import remotelogin
|
from .remotelogin import remotelogin
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
|
||||||
|
def request_username():
|
||||||
|
return request.authorization.username
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = create_app()
|
app = create_app()
|
||||||
@ -39,6 +44,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
from .kobo import kobo, get_kobo_activated
|
from .kobo import kobo, get_kobo_activated
|
||||||
from .kobo_auth import kobo_auth
|
from .kobo_auth import kobo_auth
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
kobo_available = get_kobo_activated()
|
kobo_available = get_kobo_activated()
|
||||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||||
kobo_available = False
|
kobo_available = False
|
||||||
@ -56,6 +62,7 @@ def main():
|
|||||||
app.register_blueprint(tasks)
|
app.register_blueprint(tasks)
|
||||||
app.register_blueprint(web)
|
app.register_blueprint(web)
|
||||||
app.register_blueprint(opds)
|
app.register_blueprint(opds)
|
||||||
|
limiter.limit("3/minute",key_func=request_username)(opds)
|
||||||
app.register_blueprint(jinjia)
|
app.register_blueprint(jinjia)
|
||||||
app.register_blueprint(about)
|
app.register_blueprint(about)
|
||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
@ -67,6 +74,7 @@ def main():
|
|||||||
if kobo_available:
|
if kobo_available:
|
||||||
app.register_blueprint(kobo)
|
app.register_blueprint(kobo)
|
||||||
app.register_blueprint(kobo_auth)
|
app.register_blueprint(kobo_auth)
|
||||||
|
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
@ -63,11 +63,11 @@ class Amazon(Metadata):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.warning(ex)
|
log.warning(ex)
|
||||||
return
|
return None
|
||||||
long_soup = BS(r.text, "lxml") #~4sec :/
|
long_soup = BS(r.text, "lxml") #~4sec :/
|
||||||
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
||||||
if soup2 is None:
|
if soup2 is None:
|
||||||
return
|
return None
|
||||||
try:
|
try:
|
||||||
match = MetaRecord(
|
match = MetaRecord(
|
||||||
title = "",
|
title = "",
|
||||||
@ -98,7 +98,7 @@ class Amazon(Metadata):
|
|||||||
try:
|
try:
|
||||||
match.authors = [next(
|
match.authors = [next(
|
||||||
filter(lambda i: i != " " and i != "\n" and not i.startswith("{"),
|
filter(lambda i: i != " " and i != "\n" and not i.startswith("{"),
|
||||||
x.findAll(text=True))).strip()
|
x.findAll(string=True))).strip()
|
||||||
for x in soup2.findAll("span", attrs={"class": "author"})]
|
for x in soup2.findAll("span", attrs={"class": "author"})]
|
||||||
except (AttributeError, TypeError, StopIteration):
|
except (AttributeError, TypeError, StopIteration):
|
||||||
match.authors = ""
|
match.authors = ""
|
||||||
@ -115,7 +115,7 @@ class Amazon(Metadata):
|
|||||||
return match, index
|
return match, index
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error_or_exception(e)
|
log.error_or_exception(e)
|
||||||
return
|
return None
|
||||||
|
|
||||||
val = list()
|
val = list()
|
||||||
if self.active:
|
if self.active:
|
||||||
@ -127,10 +127,10 @@ class Amazon(Metadata):
|
|||||||
results.raise_for_status()
|
results.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
log.error_or_exception(e)
|
log.error_or_exception(e)
|
||||||
return None
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
soup = BS(results.text, 'html.parser')
|
soup = BS(results.text, 'html.parser')
|
||||||
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
||||||
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
||||||
|
@ -43,7 +43,8 @@ class Douban(Metadata):
|
|||||||
__id__ = "douban"
|
__id__ = "douban"
|
||||||
DESCRIPTION = "豆瓣"
|
DESCRIPTION = "豆瓣"
|
||||||
META_URL = "https://book.douban.com/"
|
META_URL = "https://book.douban.com/"
|
||||||
SEARCH_URL = "https://www.douban.com/j/search"
|
SEARCH_JSON_URL = "https://www.douban.com/j/search"
|
||||||
|
SEARCH_URL = "https://www.douban.com/search"
|
||||||
|
|
||||||
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
|
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
|
||||||
AUTHORS_PATTERN = re.compile(r"作者|译者")
|
AUTHORS_PATTERN = re.compile(r"作者|译者")
|
||||||
@ -52,6 +53,7 @@ class Douban(Metadata):
|
|||||||
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
|
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
|
||||||
SERIES_PATTERN = re.compile(r"丛书")
|
SERIES_PATTERN = re.compile(r"丛书")
|
||||||
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
|
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
|
||||||
|
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
|
||||||
|
|
||||||
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
|
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
|
||||||
COVER_XPATH = "//a[@class='nbg']"
|
COVER_XPATH = "//a[@class='nbg']"
|
||||||
@ -66,53 +68,87 @@ class Douban(Metadata):
|
|||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
||||||
}
|
}
|
||||||
|
|
||||||
def search(
|
def search(self,
|
||||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
query: str,
|
||||||
) -> Optional[List[MetaRecord]]:
|
generic_cover: str = "",
|
||||||
|
locale: str = "en") -> List[MetaRecord]:
|
||||||
|
val = []
|
||||||
if self.active:
|
if self.active:
|
||||||
log.debug(f"starting search {query} on douban")
|
log.debug(f"start searching {query} on douban")
|
||||||
if title_tokens := list(
|
if title_tokens := list(
|
||||||
self.get_title_tokens(query, strip_joiners=False)
|
self.get_title_tokens(query, strip_joiners=False)):
|
||||||
):
|
|
||||||
query = "+".join(title_tokens)
|
query = "+".join(title_tokens)
|
||||||
|
|
||||||
|
book_id_list = self._get_book_id_list_from_html(query)
|
||||||
|
|
||||||
|
if not book_id_list:
|
||||||
|
log.debug("No search results in Douban")
|
||||||
|
return []
|
||||||
|
|
||||||
|
with futures.ThreadPoolExecutor(
|
||||||
|
max_workers=5, thread_name_prefix='douban') as executor:
|
||||||
|
|
||||||
|
fut = [
|
||||||
|
executor.submit(self._parse_single_book, book_id,
|
||||||
|
generic_cover) for book_id in book_id_list
|
||||||
|
]
|
||||||
|
|
||||||
|
val = [
|
||||||
|
future.result() for future in futures.as_completed(fut)
|
||||||
|
if future.result()
|
||||||
|
]
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
def _get_book_id_list_from_html(self, query: str) -> List[str]:
|
||||||
try:
|
try:
|
||||||
r = self.session.get(
|
r = self.session.get(self.SEARCH_URL,
|
||||||
self.SEARCH_URL, params={"cat": 1001, "q": query}
|
params={
|
||||||
)
|
"cat": 1001,
|
||||||
|
"q": query
|
||||||
|
})
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(e)
|
log.warning(e)
|
||||||
return None
|
return []
|
||||||
|
|
||||||
|
html = etree.HTML(r.content.decode("utf8"))
|
||||||
|
result_list = html.xpath(self.COVER_XPATH)
|
||||||
|
|
||||||
|
return [
|
||||||
|
self.ID_PATTERN.search(item.get("onclick")).group("id")
|
||||||
|
for item in result_list[:10]
|
||||||
|
if self.ID_PATTERN.search(item.get("onclick"))
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_book_id_list_from_json(self, query: str) -> List[str]:
|
||||||
|
try:
|
||||||
|
r = self.session.get(self.SEARCH_JSON_URL,
|
||||||
|
params={
|
||||||
|
"cat": 1001,
|
||||||
|
"q": query
|
||||||
|
})
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return []
|
||||||
|
|
||||||
results = r.json()
|
results = r.json()
|
||||||
if results["total"] == 0:
|
if results["total"] == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
book_id_list = [
|
return [
|
||||||
self.ID_PATTERN.search(item).group("id")
|
self.ID_PATTERN.search(item).group("id")
|
||||||
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
||||||
]
|
]
|
||||||
|
|
||||||
with futures.ThreadPoolExecutor(max_workers=5) as executor:
|
def _parse_single_book(self,
|
||||||
|
id: str,
|
||||||
fut = [
|
generic_cover: str = "") -> Optional[MetaRecord]:
|
||||||
executor.submit(self._parse_single_book, book_id, generic_cover)
|
|
||||||
for book_id in book_id_list
|
|
||||||
]
|
|
||||||
|
|
||||||
val = [
|
|
||||||
future.result()
|
|
||||||
for future in futures.as_completed(fut) if future.result()
|
|
||||||
]
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
def _parse_single_book(
|
|
||||||
self, id: str, generic_cover: str = ""
|
|
||||||
) -> Optional[MetaRecord]:
|
|
||||||
url = f"https://book.douban.com/subject/{id}/"
|
url = f"https://book.douban.com/subject/{id}/"
|
||||||
|
log.debug(f"start parsing {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = self.session.get(url)
|
r = self.session.get(url)
|
||||||
@ -133,10 +169,12 @@ class Douban(Metadata):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
html = etree.HTML(r.content.decode("utf8"))
|
decode_content = r.content.decode("utf8")
|
||||||
|
html = etree.HTML(decode_content)
|
||||||
|
|
||||||
match.title = html.xpath(self.TITTLE_XPATH)[0].text
|
match.title = html.xpath(self.TITTLE_XPATH)[0].text
|
||||||
match.cover = html.xpath(self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
match.cover = html.xpath(
|
||||||
|
self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
||||||
try:
|
try:
|
||||||
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
|
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -146,35 +184,39 @@ class Douban(Metadata):
|
|||||||
tag_elements = html.xpath(self.TAGS_XPATH)
|
tag_elements = html.xpath(self.TAGS_XPATH)
|
||||||
if len(tag_elements):
|
if len(tag_elements):
|
||||||
match.tags = [tag_element.text for tag_element in tag_elements]
|
match.tags = [tag_element.text for tag_element in tag_elements]
|
||||||
|
else:
|
||||||
|
match.tags = self._get_tags(decode_content)
|
||||||
|
|
||||||
description_element = html.xpath(self.DESCRIPTION_XPATH)
|
description_element = html.xpath(self.DESCRIPTION_XPATH)
|
||||||
if len(description_element):
|
if len(description_element):
|
||||||
match.description = html2text(etree.tostring(
|
match.description = html2text(
|
||||||
description_element[-1], encoding="utf8").decode("utf8"))
|
etree.tostring(description_element[-1]).decode("utf8"))
|
||||||
|
|
||||||
info = html.xpath(self.INFO_XPATH)
|
info = html.xpath(self.INFO_XPATH)
|
||||||
|
|
||||||
for element in info:
|
for element in info:
|
||||||
text = element.text
|
text = element.text
|
||||||
if self.AUTHORS_PATTERN.search(text):
|
if self.AUTHORS_PATTERN.search(text):
|
||||||
next = element.getnext()
|
next_element = element.getnext()
|
||||||
while next is not None and next.tag != "br":
|
while next_element is not None and next_element.tag != "br":
|
||||||
match.authors.append(next.text)
|
match.authors.append(next_element.text)
|
||||||
next = next.getnext()
|
next_element = next_element.getnext()
|
||||||
elif self.PUBLISHER_PATTERN.search(text):
|
elif self.PUBLISHER_PATTERN.search(text):
|
||||||
match.publisher = element.tail.strip()
|
if publisher := element.tail.strip():
|
||||||
|
match.publisher = publisher
|
||||||
|
else:
|
||||||
|
match.publisher = element.getnext().text
|
||||||
elif self.SUBTITLE_PATTERN.search(text):
|
elif self.SUBTITLE_PATTERN.search(text):
|
||||||
match.title = f'{match.title}:' + element.tail.strip()
|
match.title = f'{match.title}:{element.tail.strip()}'
|
||||||
elif self.PUBLISHED_DATE_PATTERN.search(text):
|
elif self.PUBLISHED_DATE_PATTERN.search(text):
|
||||||
match.publishedDate = self._clean_date(element.tail.strip())
|
match.publishedDate = self._clean_date(element.tail.strip())
|
||||||
elif self.SUBTITLE_PATTERN.search(text):
|
elif self.SERIES_PATTERN.search(text):
|
||||||
match.series = element.getnext().text
|
match.series = element.getnext().text
|
||||||
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
|
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
|
||||||
match.identifiers[i_type.group()] = element.tail.strip()
|
match.identifiers[i_type.group()] = element.tail.strip()
|
||||||
|
|
||||||
return match
|
return match
|
||||||
|
|
||||||
|
|
||||||
def _clean_date(self, date: str) -> str:
|
def _clean_date(self, date: str) -> str:
|
||||||
"""
|
"""
|
||||||
Clean up the date string to be in the format YYYY-MM-DD
|
Clean up the date string to be in the format YYYY-MM-DD
|
||||||
@ -194,13 +236,24 @@ class Douban(Metadata):
|
|||||||
if date[i].isdigit():
|
if date[i].isdigit():
|
||||||
digit.append(date[i])
|
digit.append(date[i])
|
||||||
elif digit:
|
elif digit:
|
||||||
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
ls.append("".join(digit) if len(digit) ==
|
||||||
|
2 else f"0{digit[0]}")
|
||||||
digit = []
|
digit = []
|
||||||
if digit:
|
if digit:
|
||||||
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
ls.append("".join(digit) if len(digit) ==
|
||||||
|
2 else f"0{digit[0]}")
|
||||||
|
|
||||||
moon = ls[0]
|
moon = ls[0]
|
||||||
if len(ls) > 1:
|
if len(ls) > 1:
|
||||||
day = ls[1]
|
day = ls[1]
|
||||||
|
|
||||||
return f"{year}-{moon}-{day}"
|
return f"{year}-{moon}-{day}"
|
||||||
|
|
||||||
|
def _get_tags(self, text: str) -> List[str]:
|
||||||
|
tags = []
|
||||||
|
if criteria := self.CRITERIA_PATTERN.search(text):
|
||||||
|
tags.extend(
|
||||||
|
item.replace('7:', '') for item in criteria.group().split('|')
|
||||||
|
if item.startswith('7:'))
|
||||||
|
|
||||||
|
return tags
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -81,7 +82,11 @@ class Google(Metadata):
|
|||||||
match.description = result["volumeInfo"].get("description", "")
|
match.description = result["volumeInfo"].get("description", "")
|
||||||
match.languages = self._parse_languages(result=result, locale=locale)
|
match.languages = self._parse_languages(result=result, locale=locale)
|
||||||
match.publisher = result["volumeInfo"].get("publisher", "")
|
match.publisher = result["volumeInfo"].get("publisher", "")
|
||||||
|
try:
|
||||||
|
datetime.strptime(result["volumeInfo"].get("publishedDate", ""), "%Y-%m-%d")
|
||||||
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
|
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
|
||||||
|
except ValueError:
|
||||||
|
match.publishedDate = ""
|
||||||
match.rating = result["volumeInfo"].get("averageRating", 0)
|
match.rating = result["volumeInfo"].get("averageRating", 0)
|
||||||
match.series, match.series_index = "", 1
|
match.series, match.series_index = "", 1
|
||||||
match.tags = result["volumeInfo"].get("categories", [])
|
match.tags = result["volumeInfo"].get("categories", [])
|
||||||
@ -103,6 +108,13 @@ class Google(Metadata):
|
|||||||
def _parse_cover(result: Dict, generic_cover: str) -> str:
|
def _parse_cover(result: Dict, generic_cover: str) -> str:
|
||||||
if result["volumeInfo"].get("imageLinks"):
|
if result["volumeInfo"].get("imageLinks"):
|
||||||
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
|
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
|
||||||
|
|
||||||
|
# strip curl in cover
|
||||||
|
cover_url = cover_url.replace("&edge=curl", "")
|
||||||
|
|
||||||
|
# request 800x900 cover image (higher resolution)
|
||||||
|
cover_url += "&fife=w800-h900"
|
||||||
|
|
||||||
return cover_url.replace("http://", "https://")
|
return cover_url.replace("http://", "https://")
|
||||||
return generic_cover
|
return generic_cover
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ class LubimyCzytac(Metadata):
|
|||||||
PUBLISH_DATE = "//dt[contains(@title,'Data pierwszego wydania"
|
PUBLISH_DATE = "//dt[contains(@title,'Data pierwszego wydania"
|
||||||
FIRST_PUBLISH_DATE = f"{DETAILS}{PUBLISH_DATE} oryginalnego')]{SIBLINGS}[1]/text()"
|
FIRST_PUBLISH_DATE = f"{DETAILS}{PUBLISH_DATE} oryginalnego')]{SIBLINGS}[1]/text()"
|
||||||
FIRST_PUBLISH_DATE_PL = f"{DETAILS}{PUBLISH_DATE} polskiego')]{SIBLINGS}[1]/text()"
|
FIRST_PUBLISH_DATE_PL = f"{DETAILS}{PUBLISH_DATE} polskiego')]{SIBLINGS}[1]/text()"
|
||||||
TAGS = "//nav[@aria-label='breadcrumb']//a[contains(@href,'/ksiazki/k/')]/text()"
|
TAGS = "//nav[@aria-label='breadcrumbs']//a[contains(@href,'/ksiazki/k/')]/span/text()"
|
||||||
|
|
||||||
RATING = "//meta[@property='books:rating:value']/@content"
|
RATING = "//meta[@property='books:rating:value']/@content"
|
||||||
COVER = "//meta[@property='og:image']/@content"
|
COVER = "//meta[@property='og:image']/@content"
|
||||||
|
@ -74,7 +74,7 @@ def register_user_with_oauth(user=None):
|
|||||||
if len(all_oauth.keys()) == 0:
|
if len(all_oauth.keys()) == 0:
|
||||||
return
|
return
|
||||||
if user is None:
|
if user is None:
|
||||||
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
flash(_("Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
|
||||||
else:
|
else:
|
||||||
for oauth_key in all_oauth.keys():
|
for oauth_key in all_oauth.keys():
|
||||||
# Find this OAuth token in the database, or create it
|
# Find this OAuth token in the database, or create it
|
||||||
@ -134,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
|||||||
# already bind with user, just login
|
# already bind with user, just login
|
||||||
if oauth_entry.user:
|
if oauth_entry.user:
|
||||||
login_user(oauth_entry.user)
|
login_user(oauth_entry.user)
|
||||||
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
|
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
|
flash(_("Success! You are now logged in as: %(nickname)s", nickname= oauth_entry.user.name),
|
||||||
category="success")
|
category="success")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
else:
|
else:
|
||||||
@ -145,21 +145,21 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
|||||||
try:
|
try:
|
||||||
ub.session.add(oauth_entry)
|
ub.session.add(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
flash(_("Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||||
log.info("Link to {} Succeeded".format(provider_name))
|
log.info("Link to {} Succeeded".format(provider_name))
|
||||||
return redirect(url_for('web.profile'))
|
return redirect(url_for('web.profile'))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
else:
|
else:
|
||||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
flash(_("Login failed, No User Linked With OAuth Account"), category="error")
|
||||||
log.info('Login failed, No User Linked With OAuth Account')
|
log.info('Login failed, No User Linked With OAuth Account')
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
# return redirect(url_for('web.login'))
|
# return redirect(url_for('web.login'))
|
||||||
# if config.config_public_reg:
|
# if config.config_public_reg:
|
||||||
# return redirect(url_for('web.register'))
|
# return redirect(url_for('web.register'))
|
||||||
# else:
|
# else:
|
||||||
# flash(_(u"Public registration is not enabled"), category="error")
|
# flash(_("Public registration is not enabled"), category="error")
|
||||||
# return redirect(url_for(redirect_url))
|
# return redirect(url_for(redirect_url))
|
||||||
except (NoResultFound, AttributeError):
|
except (NoResultFound, AttributeError):
|
||||||
return redirect(url_for(redirect_url))
|
return redirect(url_for(redirect_url))
|
||||||
@ -194,15 +194,15 @@ def unlink_oauth(provider):
|
|||||||
ub.session.delete(oauth_entry)
|
ub.session.delete(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
logout_oauth_user()
|
logout_oauth_user()
|
||||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
flash(_("Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||||
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
|
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
flash(_("Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
||||||
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
flash(_("Not Linked to %(oauth)s", oauth=provider), category="error")
|
||||||
return redirect(url_for('web.profile'))
|
return redirect(url_for('web.profile'))
|
||||||
|
|
||||||
def generate_oauth_blueprints():
|
def generate_oauth_blueprints():
|
||||||
@ -258,13 +258,13 @@ if ub.oauth_support:
|
|||||||
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
def github_logged_in(blueprint, token):
|
def github_logged_in(blueprint, token):
|
||||||
if not token:
|
if not token:
|
||||||
flash(_(u"Failed to log in with GitHub."), category="error")
|
flash(_("Failed to log in with GitHub."), category="error")
|
||||||
log.error("Failed to log in with GitHub")
|
log.error("Failed to log in with GitHub")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
resp = blueprint.session.get("/user")
|
resp = blueprint.session.get("/user")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
flash(_("Failed to fetch user info from GitHub."), category="error")
|
||||||
log.error("Failed to fetch user info from GitHub")
|
log.error("Failed to fetch user info from GitHub")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -276,13 +276,13 @@ if ub.oauth_support:
|
|||||||
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
|
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
|
||||||
def google_logged_in(blueprint, token):
|
def google_logged_in(blueprint, token):
|
||||||
if not token:
|
if not token:
|
||||||
flash(_(u"Failed to log in with Google."), category="error")
|
flash(_("Failed to log in with Google."), category="error")
|
||||||
log.error("Failed to log in with Google")
|
log.error("Failed to log in with Google")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
flash(_(u"Failed to fetch user info from Google."), category="error")
|
flash(_("Failed to fetch user info from Google."), category="error")
|
||||||
log.error("Failed to fetch user info from Google")
|
log.error("Failed to fetch user info from Google")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -295,8 +295,8 @@ if ub.oauth_support:
|
|||||||
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
def github_error(blueprint, error, error_description=None, error_uri=None):
|
def github_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
msg = (
|
msg = (
|
||||||
u"OAuth error from {name}! "
|
"OAuth error from {name}! "
|
||||||
u"error={error} description={description} uri={uri}"
|
"error={error} description={description} uri={uri}"
|
||||||
).format(
|
).format(
|
||||||
name=blueprint.name,
|
name=blueprint.name,
|
||||||
error=error,
|
error=error,
|
||||||
@ -308,8 +308,8 @@ if ub.oauth_support:
|
|||||||
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
||||||
def google_error(blueprint, error, error_description=None, error_uri=None):
|
def google_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
msg = (
|
msg = (
|
||||||
u"OAuth error from {name}! "
|
"OAuth error from {name}! "
|
||||||
u"error={error} description={description} uri={uri}"
|
"error={error} description={description} uri={uri}"
|
||||||
).format(
|
).format(
|
||||||
name=blueprint.name,
|
name=blueprint.name,
|
||||||
error=error,
|
error=error,
|
||||||
@ -329,10 +329,10 @@ def github_login():
|
|||||||
if account_info.ok:
|
if account_info.ok:
|
||||||
account_info_json = account_info.json()
|
account_info_json = account_info.json()
|
||||||
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
||||||
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
flash(_("GitHub Oauth error, please retry later."), category="error")
|
||||||
log.error("GitHub Oauth error, please retry later")
|
log.error("GitHub Oauth error, please retry later")
|
||||||
except (InvalidGrantError, TokenExpiredError) as e:
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
|
flash(_("GitHub Oauth error: {}").format(e), category="error")
|
||||||
log.error(e)
|
log.error(e)
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
@ -353,10 +353,10 @@ def google_login():
|
|||||||
if resp.ok:
|
if resp.ok:
|
||||||
account_info_json = resp.json()
|
account_info_json = resp.json()
|
||||||
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
||||||
flash(_(u"Google Oauth error, please retry later."), category="error")
|
flash(_("Google Oauth error, please retry later."), category="error")
|
||||||
log.error("Google Oauth error, please retry later")
|
log.error("Google Oauth error, please retry later")
|
||||||
except (InvalidGrantError, TokenExpiredError) as e:
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
flash(_(u"Google Oauth error: {}").format(e), category="error")
|
flash(_("Google Oauth error: {}").format(e), category="error")
|
||||||
log.error(e)
|
log.error(e)
|
||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
70
cps/opds.py
70
cps/opds.py
@ -21,41 +21,28 @@
|
|||||||
# 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 datetime
|
import datetime
|
||||||
|
import json
|
||||||
from urllib.parse import unquote_plus
|
from urllib.parse import unquote_plus
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, Response, g, make_response, abort
|
from flask import Blueprint, request, render_template, make_response, abort, Response
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
from werkzeug.security import check_password_hash
|
|
||||||
|
|
||||||
from . import constants, logger, config, db, calibre_db, ub, services, isoLanguages
|
from . import logger, config, db, calibre_db, ub, isoLanguages
|
||||||
|
from .usermanagement import requires_basic_auth_if_no_ano
|
||||||
from .helper import get_download_link, get_book_cover
|
from .helper import get_download_link, get_book_cover
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .web import render_read_books
|
from .web import render_read_books
|
||||||
from .usermanagement import load_user_from_request
|
|
||||||
from flask_babel import gettext as _
|
|
||||||
|
|
||||||
opds = Blueprint('opds', __name__)
|
opds = Blueprint('opds', __name__)
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
def requires_basic_auth_if_no_ano(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
auth = request.authorization
|
|
||||||
if config.config_anonbrowse != 1:
|
|
||||||
if not auth or auth.type != 'basic' or not check_auth(auth.username, auth.password):
|
|
||||||
return authenticate()
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and config.config_anonbrowse != 1:
|
|
||||||
return services.ldap.basic_auth_required(f)
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/")
|
@opds.route("/opds/")
|
||||||
@opds.route("/opds")
|
@opds.route("/opds")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
@ -69,7 +56,7 @@ def feed_osd():
|
|||||||
return render_xml_template('osd.xml', lang='en-EN')
|
return render_xml_template('osd.xml', lang='en-EN')
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/search", defaults={'query': ""})
|
# @opds.route("/opds/search", defaults={'query': ""})
|
||||||
@opds.route("/opds/search/<path:query>")
|
@opds.route("/opds/search/<path:query>")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_cc_search(query):
|
def feed_cc_search(query):
|
||||||
@ -328,7 +315,7 @@ def feed_format(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_languagesindex():
|
def feed_languagesindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == "all":
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
else:
|
else:
|
||||||
languages = calibre_db.session.query(db.Languages).filter(
|
languages = calibre_db.session.query(db.Languages).filter(
|
||||||
@ -355,7 +342,8 @@ def feed_languages(book_id):
|
|||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_shelfindex():
|
def feed_shelfindex():
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
shelf = g.shelves_access
|
shelf = ub.session.query(ub.Shelf).filter(
|
||||||
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||||
number = len(shelf)
|
number = len(shelf)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
number)
|
number)
|
||||||
@ -402,11 +390,7 @@ def feed_shelf(book_id):
|
|||||||
@opds.route("/opds/download/<book_id>/<book_format>/")
|
@opds.route("/opds/download/<book_id>/<book_format>/")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def opds_download_link(book_id, book_format):
|
def opds_download_link(book_id, book_format):
|
||||||
# I gave up with this: With enabled ldap login, the user doesn't get logged in, therefore it's always guest
|
if not current_user.role_download():
|
||||||
# workaround, loading the user from the request and checking its download rights here
|
|
||||||
# in case of anonymous browsing user is None
|
|
||||||
user = load_user_from_request(request) or current_user
|
|
||||||
if not user.role_download():
|
|
||||||
return abort(403)
|
return abort(403)
|
||||||
if "Kobo" in request.headers.get('User-Agent'):
|
if "Kobo" in request.headers.get('User-Agent'):
|
||||||
client = "kobo"
|
client = "kobo"
|
||||||
@ -429,6 +413,17 @@ def get_metadata_calibre_companion(uuid, library):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/stats")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def get_database_stats():
|
||||||
|
stat = dict()
|
||||||
|
stat['books'] = calibre_db.session.query(db.Books).count()
|
||||||
|
stat['authors'] = calibre_db.session.query(db.Authors).count()
|
||||||
|
stat['categories'] = calibre_db.session.query(db.Tags).count()
|
||||||
|
stat['series'] = calibre_db.session.query(db.Series).count()
|
||||||
|
return Response(json.dumps(stat), mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/thumb_240_240/<book_id>")
|
@opds.route("/opds/thumb_240_240/<book_id>")
|
||||||
@opds.route("/opds/cover_240_240/<book_id>")
|
@opds.route("/opds/cover_240_240/<book_id>")
|
||||||
@opds.route("/opds/cover_90_90/<book_id>")
|
@opds.route("/opds/cover_90_90/<book_id>")
|
||||||
@ -478,27 +473,6 @@ def feed_search(term):
|
|||||||
return render_xml_template('feed.xml', searchterm="")
|
return render_xml_template('feed.xml', searchterm="")
|
||||||
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
|
||||||
try:
|
|
||||||
username = username.encode('windows-1252')
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
username = username.encode('utf-8')
|
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
|
||||||
username.decode('utf-8').lower()).first()
|
|
||||||
if bool(user and check_password_hash(str(user.password), password)):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
|
||||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_address)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
|
||||||
return Response(
|
|
||||||
'Could not verify your access level for that URL.\n'
|
|
||||||
'You have to login with proper credentials', 401,
|
|
||||||
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
|
||||||
|
|
||||||
|
|
||||||
def render_xml_template(*args, **kwargs):
|
def render_xml_template(*args, **kwargs):
|
||||||
# ToDo: return time in current timezone similar to %z
|
# ToDo: return time in current timezone similar to %z
|
||||||
|
@ -58,8 +58,8 @@ def remote_login():
|
|||||||
ub.session.add(auth_token)
|
ub.session.add(auth_token)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
||||||
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
log.debug("Remot Login request with token: %s", auth_token.auth_token)
|
||||||
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
|
return render_title_template('remote_login.html', title=_("Login"), token=auth_token.auth_token,
|
||||||
verify_url=verify_url, page="remotelogin")
|
verify_url=verify_url, page="remotelogin")
|
||||||
|
|
||||||
|
|
||||||
@ -71,8 +71,8 @@ def verify_token(token):
|
|||||||
|
|
||||||
# Token not found
|
# Token not found
|
||||||
if auth_token is None:
|
if auth_token is None:
|
||||||
flash(_(u"Token not found"), category="error")
|
flash(_("Token not found"), category="error")
|
||||||
log.error(u"Remote Login token not found")
|
log.error("Remote Login token not found")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
# Token expired
|
# Token expired
|
||||||
@ -80,8 +80,8 @@ def verify_token(token):
|
|||||||
ub.session.delete(auth_token)
|
ub.session.delete(auth_token)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
|
||||||
flash(_(u"Token has expired"), category="error")
|
flash(_("Token has expired"), category="error")
|
||||||
log.error(u"Remote Login token expired")
|
log.error("Remote Login token expired")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
# Update token with user information
|
# Update token with user information
|
||||||
@ -89,8 +89,8 @@ def verify_token(token):
|
|||||||
auth_token.verified = True
|
auth_token.verified = True
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
|
||||||
flash(_(u"Success! Please return to your device"), category="success")
|
flash(_("Success! Please return to your device"), category="success")
|
||||||
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
|
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ def token_verified():
|
|||||||
# Token not found
|
# Token not found
|
||||||
if auth_token is None:
|
if auth_token is None:
|
||||||
data['status'] = 'error'
|
data['status'] = 'error'
|
||||||
data['message'] = _(u"Token not found")
|
data['message'] = _("Token not found")
|
||||||
|
|
||||||
# Token expired
|
# Token expired
|
||||||
elif datetime.now() > auth_token.expiration:
|
elif datetime.now() > auth_token.expiration:
|
||||||
@ -113,7 +113,7 @@ def token_verified():
|
|||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
|
||||||
data['status'] = 'error'
|
data['status'] = 'error'
|
||||||
data['message'] = _(u"Token has expired")
|
data['message'] = _("Token has expired")
|
||||||
|
|
||||||
elif not auth_token.verified:
|
elif not auth_token.verified:
|
||||||
data['status'] = 'not_verified'
|
data['status'] = 'not_verified'
|
||||||
@ -126,8 +126,8 @@ def token_verified():
|
|||||||
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
|
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
|
||||||
|
|
||||||
data['status'] = 'success'
|
data['status'] = 'success'
|
||||||
log.debug(u"Remote Login for userid %s succeeded", user.id)
|
log.debug("Remote Login for userid %s succeeded", user.id)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success")
|
||||||
|
|
||||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
@ -20,11 +20,13 @@ from flask import render_template, g, abort, request
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
from . import config, constants, logger
|
from . import config, constants, logger, ub
|
||||||
from .ub import User
|
from .ub import User
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
def get_sidebar_config(kwargs=None):
|
def get_sidebar_config(kwargs=None):
|
||||||
@ -45,12 +47,12 @@ def get_sidebar_config(kwargs=None):
|
|||||||
"show_text": _('Show Hot Books'), "config_show": True})
|
"show_text": _('Show Hot Books'), "config_show": True})
|
||||||
if current_user.role_admin():
|
if current_user.role_admin():
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
"config_show": content})
|
"config_show": content})
|
||||||
else:
|
else:
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
"config_show": content})
|
"config_show": content})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
@ -58,47 +60,50 @@ def get_sidebar_config(kwargs=None):
|
|||||||
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
|
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
|
||||||
"show_text": _('Show Top Rated Books'), "config_show": True})
|
"show_text": _('Show Top Rated Books'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
|
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
|
||||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
|
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous),
|
||||||
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
|
"page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
|
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
|
||||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
|
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), "page": "unread",
|
||||||
"show_text": _('Show unread'), "config_show": False})
|
"show_text": _('Show unread'), "config_show": False})
|
||||||
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
||||||
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
||||||
"show_text": _('Show Random Books'), "config_show": True})
|
"show_text": _('Show Random Books'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
||||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
||||||
"show_text": _('Show category selection'), "config_show": True})
|
"show_text": _('Show Category Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
||||||
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
|
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
|
||||||
"show_text": _('Show series selection'), "config_show": True})
|
"show_text": _('Show Series Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
||||||
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
|
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
|
||||||
"show_text": _('Show author selection'), "config_show": True})
|
"show_text": _('Show Author Section'), "config_show": True})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
||||||
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
|
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
|
||||||
"show_text": _('Show publisher selection'), "config_show":True})
|
"show_text": _('Show Publisher Section'), "config_show":True})
|
||||||
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
||||||
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
|
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
|
||||||
"page": "language",
|
"page": "language",
|
||||||
"show_text": _('Show language selection'), "config_show": True})
|
"show_text": _('Show Language Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
||||||
"visibility": constants.SIDEBAR_RATING, 'public': True,
|
"visibility": constants.SIDEBAR_RATING, 'public': True,
|
||||||
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
|
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
||||||
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
|
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
|
||||||
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
|
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
||||||
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
|
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
|
||||||
"show_text": _('Show archived books'), "config_show": content})
|
"show_text": _('Show Archived Books'), "config_show": content})
|
||||||
if not simple:
|
if not simple:
|
||||||
sidebar.append(
|
sidebar.append(
|
||||||
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
||||||
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
|
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous), "page": "list",
|
||||||
"show_text": _('Show Books List'), "config_show": content})
|
"show_text": _('Show Books List'), "config_show": content})
|
||||||
|
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
||||||
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||||
|
|
||||||
return sidebar, simple
|
return sidebar, simple
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from . import config, constants
|
from . import config, constants
|
||||||
from .services.background_scheduler import BackgroundScheduler, use_APScheduler
|
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
|
||||||
from .tasks.database import TaskReconnectDatabase
|
from .tasks.database import TaskReconnectDatabase
|
||||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
@ -27,13 +27,12 @@ from .tasks.metadata_backup import TaskBackupMetadata
|
|||||||
|
|
||||||
def get_scheduled_tasks(reconnect=True):
|
def get_scheduled_tasks(reconnect=True):
|
||||||
tasks = list()
|
tasks = list()
|
||||||
# config.schedule_reconnect or
|
# Reconnect Calibre database (metadata.db) based on config.schedule_reconnect
|
||||||
# Reconnect Calibre database (metadata.db)
|
|
||||||
if reconnect:
|
if reconnect:
|
||||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||||
|
|
||||||
# ToDo make configurable. Generate metadata.opf file for each changed book
|
# Generate metadata.opf file for each changed book
|
||||||
if False:
|
if config.schedule_metadata_backup:
|
||||||
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
|
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
|
||||||
|
|
||||||
# Generate all missing book cover thumbnails
|
# Generate all missing book cover thumbnails
|
||||||
@ -66,10 +65,10 @@ def register_scheduled_tasks(reconnect=True):
|
|||||||
duration = config.schedule_duration
|
duration = config.schedule_duration
|
||||||
|
|
||||||
# Register scheduled tasks
|
# Register scheduled tasks
|
||||||
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger='cron', hour=start)
|
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start))
|
||||||
end_time = calclulate_end_time(start, duration)
|
end_time = calclulate_end_time(start, duration)
|
||||||
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end_time.hour,
|
scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute),
|
||||||
minute=end_time.minute)
|
name="end scheduled task")
|
||||||
|
|
||||||
# Kick-off tasks, if they should currently be running
|
# Kick-off tasks, if they should currently be running
|
||||||
if should_task_be_running(start, duration):
|
if should_task_be_running(start, duration):
|
||||||
|
@ -45,7 +45,7 @@ def simple_search():
|
|||||||
return render_title_template('search.html',
|
return render_title_template('search.html',
|
||||||
searchterm="",
|
searchterm="",
|
||||||
result_count=0,
|
result_count=0,
|
||||||
title=_(u"Search"),
|
title=_("Search"),
|
||||||
page="search")
|
page="search")
|
||||||
|
|
||||||
|
|
||||||
@ -185,18 +185,18 @@ def extend_search_term(searchterm,
|
|||||||
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
||||||
if pub_start:
|
if pub_start:
|
||||||
try:
|
try:
|
||||||
searchterm.extend([_(u"Published after ") +
|
searchterm.extend([_("Published after ") +
|
||||||
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
|
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
|
||||||
format='medium')])
|
format='medium')])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pub_start = u""
|
pub_start = ""
|
||||||
if pub_end:
|
if pub_end:
|
||||||
try:
|
try:
|
||||||
searchterm.extend([_(u"Published before ") +
|
searchterm.extend([_("Published before ") +
|
||||||
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
|
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
|
||||||
format='medium')])
|
format='medium')])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pub_end = u""
|
pub_end = ""
|
||||||
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
|
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
|
||||||
for key, db_element in elements.items():
|
for key, db_element in elements.items():
|
||||||
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
|
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
|
||||||
@ -214,11 +214,11 @@ def extend_search_term(searchterm,
|
|||||||
language_names = calibre_db.speaking_language(language_names)
|
language_names = calibre_db.speaking_language(language_names)
|
||||||
searchterm.extend(language.name for language in language_names)
|
searchterm.extend(language.name for language in language_names)
|
||||||
if rating_high:
|
if rating_high:
|
||||||
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
|
searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
|
||||||
if rating_low:
|
if rating_low:
|
||||||
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
|
searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
|
||||||
if read_status:
|
if read_status != "Any":
|
||||||
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
|
searchterm.extend([_("Read Status = '%(status)s'", status=read_status)])
|
||||||
searchterm.extend(ext for ext in tags['include_extension'])
|
searchterm.extend(ext for ext in tags['include_extension'])
|
||||||
searchterm.extend(ext for ext in tags['exclude_extension'])
|
searchterm.extend(ext for ext in tags['exclude_extension'])
|
||||||
# handle custom columns
|
# handle custom columns
|
||||||
@ -267,23 +267,23 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
column_start = term.get('custom_column_' + str(c.id) + '_start')
|
column_start = term.get('custom_column_' + str(c.id) + '_start')
|
||||||
column_end = term.get('custom_column_' + str(c.id) + '_end')
|
column_end = term.get('custom_column_' + str(c.id) + '_end')
|
||||||
if column_start:
|
if column_start:
|
||||||
search_term.extend([u"{} >= {}".format(c.name,
|
search_term.extend(["{} >= {}".format(c.name,
|
||||||
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
|
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
|
||||||
format='medium')
|
format='medium')
|
||||||
)])
|
)])
|
||||||
cc_present = True
|
cc_present = True
|
||||||
if column_end:
|
if column_end:
|
||||||
search_term.extend([u"{} <= {}".format(c.name,
|
search_term.extend(["{} <= {}".format(c.name,
|
||||||
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
|
||||||
format='medium')
|
format='medium')
|
||||||
)])
|
)])
|
||||||
cc_present = True
|
cc_present = True
|
||||||
elif term.get('custom_column_' + str(c.id)):
|
elif term.get('custom_column_' + str(c.id)):
|
||||||
search_term.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
|
||||||
cc_present = True
|
cc_present = True
|
||||||
|
|
||||||
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
|
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
|
||||||
or rating_high or description or cc_present or read_status:
|
or rating_high or description or cc_present or read_status != "Any":
|
||||||
search_term, pub_start, pub_end = extend_search_term(search_term,
|
search_term, pub_start, pub_end = extend_search_term(search_term,
|
||||||
author_name,
|
author_name,
|
||||||
book_title,
|
book_title,
|
||||||
@ -302,6 +302,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
|
q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
|
||||||
if pub_end:
|
if pub_end:
|
||||||
q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
|
q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
|
||||||
|
if read_status != "Any":
|
||||||
q = q.filter(adv_search_read_status(read_status))
|
q = q.filter(adv_search_read_status(read_status))
|
||||||
if publisher:
|
if publisher:
|
||||||
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
||||||
@ -339,7 +340,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
entries=entries,
|
entries=entries,
|
||||||
result_count=result_count,
|
result_count=result_count,
|
||||||
title=_(u"Advanced Search"), page="advsearch",
|
title=_("Advanced Search"), page="advsearch",
|
||||||
order=order[1])
|
order=order[1])
|
||||||
|
|
||||||
|
|
||||||
@ -366,15 +367,16 @@ def render_prepare_search_form(cc):
|
|||||||
.filter(calibre_db.common_filters()) \
|
.filter(calibre_db.common_filters()) \
|
||||||
.group_by(db.Data.format)\
|
.group_by(db.Data.format)\
|
||||||
.order_by(db.Data.format).all()
|
.order_by(db.Data.format).all()
|
||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == "all":
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
else:
|
else:
|
||||||
languages = None
|
languages = None
|
||||||
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
|
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
|
||||||
series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
|
series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
|
||||||
|
|
||||||
|
|
||||||
def render_search_results(term, offset=None, order=None, limit=None):
|
def render_search_results(term, offset=None, order=None, limit=None):
|
||||||
|
if term:
|
||||||
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
|
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
|
||||||
entries, result_count, pagination = calibre_db.get_search_results(term,
|
entries, result_count, pagination = calibre_db.get_search_results(term,
|
||||||
config,
|
config,
|
||||||
@ -382,6 +384,11 @@ def render_search_results(term, offset=None, order=None, limit=None):
|
|||||||
order,
|
order,
|
||||||
limit,
|
limit,
|
||||||
*join)
|
*join)
|
||||||
|
else:
|
||||||
|
entries = list()
|
||||||
|
order = [None, None]
|
||||||
|
pagination = result_count = None
|
||||||
|
|
||||||
return render_title_template('search.html',
|
return render_title_template('search.html',
|
||||||
searchterm=term,
|
searchterm=term,
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
@ -389,7 +396,7 @@ def render_search_results(term, offset=None, order=None, limit=None):
|
|||||||
adv_searchterm=term,
|
adv_searchterm=term,
|
||||||
entries=entries,
|
entries=entries,
|
||||||
result_count=result_count,
|
result_count=result_count,
|
||||||
title=_(u"Search"),
|
title=_("Search"),
|
||||||
page="search",
|
page="search",
|
||||||
order=order[1])
|
order=order[1])
|
||||||
|
|
||||||
|
@ -21,12 +21,12 @@ import os
|
|||||||
import errno
|
import errno
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess # nosec
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
from .gevent_wsgi import MyWSGIHandler
|
from .gevent_wsgi import MyWSGIHandler
|
||||||
from gevent.pool import Pool
|
from gevent.pool import Pool
|
||||||
|
from gevent.socket import socket as GeventSocket
|
||||||
from gevent import __version__ as _version
|
from gevent import __version__ as _version
|
||||||
from greenlet import GreenletExit
|
from greenlet import GreenletExit
|
||||||
import ssl
|
import ssl
|
||||||
@ -36,6 +36,7 @@ except ImportError:
|
|||||||
from .tornado_wsgi import MyWSGIContainer
|
from .tornado_wsgi import MyWSGIContainer
|
||||||
from tornado.httpserver import HTTPServer
|
from tornado.httpserver import HTTPServer
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
|
from tornado.netutil import bind_unix_socket
|
||||||
from tornado import version as _version
|
from tornado import version as _version
|
||||||
VERSION = 'Tornado ' + _version
|
VERSION = 'Tornado ' + _version
|
||||||
_GEVENT = False
|
_GEVENT = False
|
||||||
@ -95,7 +96,12 @@ class WebServer(object):
|
|||||||
log.warning('Cert path: %s', certfile_path)
|
log.warning('Cert path: %s', certfile_path)
|
||||||
log.warning('Key path: %s', keyfile_path)
|
log.warning('Key path: %s', keyfile_path)
|
||||||
|
|
||||||
def _make_gevent_unix_socket(self, socket_file):
|
def _make_gevent_socket_activated(self):
|
||||||
|
# Reuse an already open socket on fd=SD_LISTEN_FDS_START
|
||||||
|
SD_LISTEN_FDS_START = 3
|
||||||
|
return GeventSocket(fileno=SD_LISTEN_FDS_START)
|
||||||
|
|
||||||
|
def _prepare_unix_socket(self, socket_file):
|
||||||
# the socket file must not exist prior to bind()
|
# the socket file must not exist prior to bind()
|
||||||
if os.path.exists(socket_file):
|
if os.path.exists(socket_file):
|
||||||
# avoid nuking regular files and symbolic links (could be a mistype or security issue)
|
# avoid nuking regular files and symbolic links (could be a mistype or security issue)
|
||||||
@ -103,35 +109,41 @@ class WebServer(object):
|
|||||||
raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), socket_file)
|
raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), socket_file)
|
||||||
os.remove(socket_file)
|
os.remove(socket_file)
|
||||||
|
|
||||||
unix_sock = WSGIServer.get_listener(socket_file, family=socket.AF_UNIX)
|
|
||||||
self.unix_socket_file = socket_file
|
self.unix_socket_file = socket_file
|
||||||
|
|
||||||
|
def _make_gevent_listener(self):
|
||||||
|
if os.name != 'nt':
|
||||||
|
socket_activated = os.environ.get("LISTEN_FDS")
|
||||||
|
if socket_activated:
|
||||||
|
sock = self._make_gevent_socket_activated()
|
||||||
|
sock_info = sock.getsockname()
|
||||||
|
return sock, "systemd-socket:" + _readable_listen_address(sock_info[0], sock_info[1])
|
||||||
|
unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET")
|
||||||
|
if unix_socket_file:
|
||||||
|
self._prepare_unix_socket(unix_socket_file)
|
||||||
|
unix_sock = WSGIServer.get_listener(unix_socket_file, family=socket.AF_UNIX)
|
||||||
# ensure current user and group have r/w permissions, no permissions for other users
|
# ensure current user and group have r/w permissions, no permissions for other users
|
||||||
# this way the socket can be shared in a semi-secure manner
|
# this way the socket can be shared in a semi-secure manner
|
||||||
# between the user running calibre-web and the user running the fronting webserver
|
# between the user running calibre-web and the user running the fronting webserver
|
||||||
os.chmod(socket_file, 0o660)
|
os.chmod(unix_socket_file, 0o660)
|
||||||
|
|
||||||
return unix_sock
|
return unix_sock, "unix:" + unix_socket_file
|
||||||
|
|
||||||
def _make_gevent_socket(self):
|
|
||||||
if os.name != 'nt':
|
|
||||||
unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET")
|
|
||||||
if unix_socket_file:
|
|
||||||
return self._make_gevent_unix_socket(unix_socket_file), "unix:" + unix_socket_file
|
|
||||||
|
|
||||||
if self.listen_address:
|
if self.listen_address:
|
||||||
return (self.listen_address, self.listen_port), None
|
return ((self.listen_address, self.listen_port),
|
||||||
|
_readable_listen_address(self.listen_address, self.listen_port))
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
self.listen_address = '0.0.0.0'
|
self.listen_address = '0.0.0.0'
|
||||||
return (self.listen_address, self.listen_port), None
|
return ((self.listen_address, self.listen_port),
|
||||||
|
_readable_listen_address(self.listen_address, self.listen_port))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
address = ('::', self.listen_port)
|
address = ('::', self.listen_port)
|
||||||
sock = WSGIServer.get_listener(address, family=socket.AF_INET6)
|
sock = WSGIServer.get_listener(address, family=socket.AF_INET6)
|
||||||
except socket.error as ex:
|
except socket.error as ex:
|
||||||
log.error('%s', ex)
|
log.error('%s', ex)
|
||||||
log.warning('Unable to listen on "", trying on IPv4 only...')
|
log.warning('Unable to listen on {}, trying on IPv4 only...'.format(address))
|
||||||
address = ('', self.listen_port)
|
address = ('', self.listen_port)
|
||||||
sock = WSGIServer.get_listener(address, family=socket.AF_INET)
|
sock = WSGIServer.get_listener(address, family=socket.AF_INET)
|
||||||
|
|
||||||
@ -152,7 +164,7 @@ class WebServer(object):
|
|||||||
# The value of __package__ indicates how Python was called. It may
|
# The value of __package__ indicates how Python was called. It may
|
||||||
# not exist if a setuptools script is installed as an egg. It may be
|
# not exist if a setuptools script is installed as an egg. It may be
|
||||||
# set incorrectly for entry points created with pip on Windows.
|
# set incorrectly for entry points created with pip on Windows.
|
||||||
if getattr(__main__, "__package__", None) is None or (
|
if getattr(__main__, "__package__", "") in ["", None] or (
|
||||||
os.name == "nt"
|
os.name == "nt"
|
||||||
and __main__.__package__ == ""
|
and __main__.__package__ == ""
|
||||||
and not os.path.exists(py_script)
|
and not os.path.exists(py_script)
|
||||||
@ -193,15 +205,15 @@ class WebServer(object):
|
|||||||
rv.extend(("-m", py_module.lstrip(".")))
|
rv.extend(("-m", py_module.lstrip(".")))
|
||||||
|
|
||||||
rv.extend(args)
|
rv.extend(args)
|
||||||
|
if os.name == 'nt':
|
||||||
|
rv = ['"{}"'.format(a) for a in rv]
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def _start_gevent(self):
|
def _start_gevent(self):
|
||||||
ssl_args = self.ssl_args or {}
|
ssl_args = self.ssl_args or {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock, output = self._make_gevent_socket()
|
sock, output = self._make_gevent_listener()
|
||||||
if output is None:
|
|
||||||
output = _readable_listen_address(self.listen_address, self.listen_port)
|
|
||||||
log.info('Starting Gevent server on %s', output)
|
log.info('Starting Gevent server on %s', output)
|
||||||
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
|
||||||
error_log=log,
|
error_log=log,
|
||||||
@ -226,17 +238,42 @@ class WebServer(object):
|
|||||||
if os.name == 'nt' and sys.version_info > (3, 7):
|
if os.name == 'nt' and sys.version_info > (3, 7):
|
||||||
import asyncio
|
import asyncio
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port))
|
try:
|
||||||
|
|
||||||
# Max Buffersize set to 200MB
|
# Max Buffersize set to 200MB
|
||||||
http_server = HTTPServer(MyWSGIContainer(self.app),
|
http_server = HTTPServer(MyWSGIContainer(self.app),
|
||||||
max_buffer_size=209700000,
|
max_buffer_size=209700000,
|
||||||
ssl_options=self.ssl_args)
|
ssl_options=self.ssl_args)
|
||||||
|
|
||||||
|
unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET")
|
||||||
|
if os.environ.get("LISTEN_FDS") and os.name != 'nt':
|
||||||
|
SD_LISTEN_FDS_START = 3
|
||||||
|
sock = socket.socket(fileno=SD_LISTEN_FDS_START)
|
||||||
|
http_server.add_socket(sock)
|
||||||
|
sock.setblocking(0)
|
||||||
|
socket_name =sock.getsockname()
|
||||||
|
output = "systemd-socket:" + _readable_listen_address(socket_name[0], socket_name[1])
|
||||||
|
elif unix_socket_file and os.name != 'nt':
|
||||||
|
self._prepare_unix_socket(unix_socket_file)
|
||||||
|
output = "unix:" + unix_socket_file
|
||||||
|
unix_socket = bind_unix_socket(self.unix_socket_file)
|
||||||
|
http_server.add_socket(unix_socket)
|
||||||
|
# ensure current user and group have r/w permissions, no permissions for other users
|
||||||
|
# this way the socket can be shared in a semi-secure manner
|
||||||
|
# between the user running calibre-web and the user running the fronting webserver
|
||||||
|
os.chmod(self.unix_socket_file, 0o660)
|
||||||
|
else:
|
||||||
|
output = _readable_listen_address(self.listen_address, self.listen_port)
|
||||||
http_server.listen(self.listen_port, self.listen_address)
|
http_server.listen(self.listen_port, self.listen_address)
|
||||||
|
log.info('Starting Tornado server on %s', output)
|
||||||
|
|
||||||
self.wsgiserver = IOLoop.current()
|
self.wsgiserver = IOLoop.current()
|
||||||
self.wsgiserver.start()
|
self.wsgiserver.start()
|
||||||
# wait for stop signal
|
# wait for stop signal
|
||||||
self.wsgiserver.close(True)
|
self.wsgiserver.close(True)
|
||||||
|
finally:
|
||||||
|
if self.unix_socket_file:
|
||||||
|
os.remove(self.unix_socket_file)
|
||||||
|
self.unix_socket_file = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
try:
|
try:
|
||||||
@ -262,9 +299,16 @@ class WebServer(object):
|
|||||||
|
|
||||||
log.info("Performing restart of Calibre-Web")
|
log.info("Performing restart of Calibre-Web")
|
||||||
args = self._get_args_for_reloading()
|
args = self._get_args_for_reloading()
|
||||||
subprocess.call(args, close_fds=True) # nosec
|
os.execv(args[0].lstrip('"').rstrip('"'), args)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shutdown_scheduler():
|
||||||
|
from .services.background_scheduler import BackgroundScheduler
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
if scheduler:
|
||||||
|
scheduler.scheduler.shutdown()
|
||||||
|
|
||||||
def _killServer(self, __, ___):
|
def _killServer(self, __, ___):
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
@ -273,9 +317,13 @@ class WebServer(object):
|
|||||||
updater_thread.stop()
|
updater_thread.stop()
|
||||||
|
|
||||||
log.info("webserver stop (restart=%s)", restart)
|
log.info("webserver stop (restart=%s)", restart)
|
||||||
|
self.shutdown_scheduler()
|
||||||
self.restart = restart
|
self.restart = restart
|
||||||
if self.wsgiserver:
|
if self.wsgiserver:
|
||||||
if _GEVENT:
|
if _GEVENT:
|
||||||
self.wsgiserver.close()
|
self.wsgiserver.close()
|
||||||
|
else:
|
||||||
|
if restart:
|
||||||
|
self.wsgiserver.call_later(1.0, self.wsgiserver.stop)
|
||||||
else:
|
else:
|
||||||
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
|
||||||
|
@ -19,11 +19,9 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from jsonschema import validate, exceptions, __version__
|
from jsonschema import validate, exceptions
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
from flask import json
|
from flask import json
|
||||||
from .. import logger
|
from .. import logger
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ from .worker import WorkerThread
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from apscheduler.triggers.date import DateTrigger
|
||||||
use_APScheduler = True
|
use_APScheduler = True
|
||||||
except (ImportError, RuntimeError) as e:
|
except (ImportError, RuntimeError) as e:
|
||||||
use_APScheduler = False
|
use_APScheduler = False
|
||||||
@ -43,35 +45,33 @@ class BackgroundScheduler:
|
|||||||
cls.scheduler = BScheduler()
|
cls.scheduler = BScheduler()
|
||||||
cls.scheduler.start()
|
cls.scheduler.start()
|
||||||
|
|
||||||
atexit.register(lambda: cls.scheduler.shutdown())
|
|
||||||
|
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def schedule(self, func, trigger, name=None, **trigger_args):
|
def schedule(self, func, trigger, name=None):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args)
|
return self.scheduler.add_job(func=func, trigger=trigger, name=name)
|
||||||
|
|
||||||
# Expects a lambda expression for the task
|
# Expects a lambda expression for the task
|
||||||
def schedule_task(self, task, user=None, name=None, hidden=False, trigger='cron', **trigger_args):
|
def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
def scheduled_task():
|
def scheduled_task():
|
||||||
worker_task = task()
|
worker_task = task()
|
||||||
worker_task.scheduled = True
|
worker_task.scheduled = True
|
||||||
WorkerThread.add(user, worker_task, hidden=hidden)
|
WorkerThread.add(user, worker_task, hidden=hidden)
|
||||||
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args)
|
return self.schedule(func=scheduled_task, trigger=trigger, name=name)
|
||||||
|
|
||||||
# Expects a list of lambda expressions for the tasks
|
# Expects a list of lambda expressions for the tasks
|
||||||
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
|
def schedule_tasks(self, tasks, user=None, trigger=None):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2], **trigger_args)
|
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
|
||||||
|
|
||||||
# Expects a lambda expression for the task
|
# Expects a lambda expression for the task
|
||||||
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
|
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
def immediate_task():
|
def immediate_task():
|
||||||
WorkerThread.add(user, task(), hidden)
|
WorkerThread.add(user, task(), hidden)
|
||||||
return self.schedule(func=immediate_task, trigger='date', name=name)
|
return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
|
||||||
|
|
||||||
# Expects a list of lambda expressions for the tasks
|
# Expects a list of lambda expressions for the tasks
|
||||||
def schedule_tasks_immediately(self, tasks, user=None):
|
def schedule_tasks_immediately(self, tasks, user=None):
|
||||||
|
@ -20,6 +20,7 @@ import base64
|
|||||||
|
|
||||||
from flask_simpleldap import LDAP, LDAPException
|
from flask_simpleldap import LDAP, LDAPException
|
||||||
from flask_simpleldap import ldap as pyLDAP
|
from flask_simpleldap import ldap as pyLDAP
|
||||||
|
from flask import current_app
|
||||||
from .. import constants, logger
|
from .. import constants, logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -28,8 +29,47 @@ except ImportError:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
_ldap = LDAP()
|
|
||||||
|
|
||||||
|
class LDAPLogger(object):
|
||||||
|
|
||||||
|
def write(self, message):
|
||||||
|
try:
|
||||||
|
log.debug(message.strip("\n").replace("\n", ""))
|
||||||
|
except Exception:
|
||||||
|
log.debug("Logging Error")
|
||||||
|
|
||||||
|
|
||||||
|
class mySimpleLDap(LDAP):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_app(app):
|
||||||
|
super(mySimpleLDap, mySimpleLDap).init_app(app)
|
||||||
|
app.config.setdefault('LDAP_LOGLEVEL', 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initialize(self):
|
||||||
|
"""Initialize a connection to the LDAP server.
|
||||||
|
|
||||||
|
:return: LDAP connection object.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
log_level = 2 if current_app.config['LDAP_LOGLEVEL'] == logger.logging.DEBUG else 0
|
||||||
|
conn = pyLDAP.initialize('{0}://{1}:{2}'.format(
|
||||||
|
current_app.config['LDAP_SCHEMA'],
|
||||||
|
current_app.config['LDAP_HOST'],
|
||||||
|
current_app.config['LDAP_PORT']), trace_level=log_level, trace_file=LDAPLogger())
|
||||||
|
conn.set_option(pyLDAP.OPT_NETWORK_TIMEOUT,
|
||||||
|
current_app.config['LDAP_TIMEOUT'])
|
||||||
|
conn = self._set_custom_options(conn)
|
||||||
|
conn.protocol_version = pyLDAP.VERSION3
|
||||||
|
if current_app.config['LDAP_USE_TLS']:
|
||||||
|
conn.start_tls_s()
|
||||||
|
return conn
|
||||||
|
except pyLDAP.LDAPError as e:
|
||||||
|
raise LDAPException(self.error(e.args))
|
||||||
|
|
||||||
|
|
||||||
|
_ldap = mySimpleLDap()
|
||||||
|
|
||||||
def init_app(app, config):
|
def init_app(app, config):
|
||||||
if config.config_login_type != constants.LOGIN_LDAP:
|
if config.config_login_type != constants.LOGIN_LDAP:
|
||||||
@ -44,15 +84,15 @@ def init_app(app, config):
|
|||||||
app.config['LDAP_SCHEMA'] = 'ldap'
|
app.config['LDAP_SCHEMA'] = 'ldap'
|
||||||
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
|
||||||
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
|
||||||
if config.config_ldap_serv_password is None:
|
if config.config_ldap_serv_password_e is None:
|
||||||
config.config_ldap_serv_password = ''
|
config.config_ldap_serv_password_e = ''
|
||||||
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
|
app.config['LDAP_PASSWORD'] = config.config_ldap_serv_password_e
|
||||||
else:
|
else:
|
||||||
app.config['LDAP_PASSWORD'] = base64.b64decode("")
|
app.config['LDAP_PASSWORD'] = ""
|
||||||
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
|
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
|
||||||
else:
|
else:
|
||||||
app.config['LDAP_USERNAME'] = ""
|
app.config['LDAP_USERNAME'] = ""
|
||||||
app.config['LDAP_PASSWORD'] = base64.b64decode("")
|
app.config['LDAP_PASSWORD'] = ""
|
||||||
if bool(config.config_ldap_cert_path):
|
if bool(config.config_ldap_cert_path):
|
||||||
app.config['LDAP_CUSTOM_OPTIONS'].update({
|
app.config['LDAP_CUSTOM_OPTIONS'].update({
|
||||||
pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND,
|
pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND,
|
||||||
@ -70,7 +110,7 @@ def init_app(app, config):
|
|||||||
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
|
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
|
||||||
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
|
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
|
||||||
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
|
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
|
||||||
|
app.config['LDAP_LOGLEVEL'] = config.config_log_level
|
||||||
try:
|
try:
|
||||||
_ldap.init_app(app)
|
_ldap.init_app(app)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
65
cps/shelf.py
65
cps/shelf.py
@ -46,13 +46,13 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
if shelf is None:
|
if shelf is None:
|
||||||
log.error("Invalid shelf specified: %s", shelf_id)
|
log.error("Invalid shelf specified: %s", shelf_id)
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Invalid shelf specified"), category="error")
|
flash(_("Invalid shelf specified"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Invalid shelf specified", 400
|
return "Invalid shelf specified", 400
|
||||||
|
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
|
flash(_("Sorry you are not allowed to add a book to that shelf"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Sorry you are not allowed to add a book to the that shelf", 403
|
return "Sorry you are not allowed to add a book to the that shelf", 403
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
if book_in_shelf:
|
if book_in_shelf:
|
||||||
log.error("Book %s is already part of %s", book_id, shelf)
|
log.error("Book %s is already part of %s", book_id, shelf)
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
|
flash(_("Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Book is already part of the shelf: %s" % shelf.name, 400
|
return "Book is already part of the shelf: %s" % shelf.name, 400
|
||||||
|
|
||||||
@ -79,14 +79,14 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
log.debug("Book has been added to shelf: {}".format(shelf.name))
|
log.debug("Book has been added to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_("Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
@ -100,12 +100,12 @@ def search_to_shelf(shelf_id):
|
|||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf is None:
|
if shelf is None:
|
||||||
log.error("Invalid shelf specified: {}".format(shelf_id))
|
log.error("Invalid shelf specified: {}".format(shelf_id))
|
||||||
flash(_(u"Invalid shelf specified"), category="error")
|
flash(_("Invalid shelf specified"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
|
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
|
||||||
flash(_(u"You are not allowed to add a book to the shelf"), category="error")
|
flash(_("You are not allowed to add a book to the shelf"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
||||||
@ -123,7 +123,7 @@ def search_to_shelf(shelf_id):
|
|||||||
|
|
||||||
if not books_for_shelf:
|
if not books_for_shelf:
|
||||||
log.error("Books are already part of {}".format(shelf.name))
|
log.error("Books are already part of {}".format(shelf.name))
|
||||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_("Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
||||||
@ -135,14 +135,14 @@ def search_to_shelf(shelf_id):
|
|||||||
try:
|
try:
|
||||||
ub.session.merge(shelf)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_("Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
else:
|
else:
|
||||||
log.error("Could not add books to shelf: {}".format(shelf.name))
|
log.error("Could not add books to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
flash(_("Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@ -182,13 +182,13 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_("Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
@ -197,7 +197,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
else:
|
else:
|
||||||
if not xhr:
|
if not xhr:
|
||||||
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf"),
|
flash(_("Sorry you are not allowed to remove a book from this shelf"),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Sorry you are not allowed to remove a book from this shelf", 403
|
return "Sorry you are not allowed to remove a book from this shelf", 403
|
||||||
@ -207,7 +207,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def create_shelf():
|
def create_shelf():
|
||||||
shelf = ub.Shelf()
|
shelf = ub.Shelf()
|
||||||
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
|
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@ -215,9 +215,9 @@ def create_shelf():
|
|||||||
def edit_shelf(shelf_id):
|
def edit_shelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
|
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
return create_edit_shelf(shelf, page_title=_("Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||||
@ -232,7 +232,7 @@ def delete_shelf(shelf_id):
|
|||||||
except InvalidRequestError as e:
|
except InvalidRequestError as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@ -269,7 +269,7 @@ def order_shelf(shelf_id):
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
|
|
||||||
result = list()
|
result = list()
|
||||||
if shelf:
|
if shelf:
|
||||||
@ -278,7 +278,7 @@ def order_shelf(shelf_id):
|
|||||||
.add_columns(calibre_db.common_filters().label("visible")) \
|
.add_columns(calibre_db.common_filters().label("visible")) \
|
||||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
||||||
return render_title_template('shelf_order.html', entries=result,
|
return render_title_template('shelf_order.html', entries=result,
|
||||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
title=_("Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf, page="shelforder")
|
shelf=shelf, page="shelforder")
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
@ -295,11 +295,14 @@ def check_shelf_edit_permissions(cur_shelf):
|
|||||||
|
|
||||||
|
|
||||||
def check_shelf_view_permissions(cur_shelf):
|
def check_shelf_view_permissions(cur_shelf):
|
||||||
|
try:
|
||||||
if cur_shelf.is_public:
|
if cur_shelf.is_public:
|
||||||
return True
|
return True
|
||||||
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
||||||
log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
|
log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
|
||||||
return False
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.error(e)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -310,7 +313,7 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
|
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
|
||||||
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
|
flash(_("Sorry you are not allowed to create a public shelf"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
is_public = 1 if to_save.get("is_public") == "on" else 0
|
is_public = 1 if to_save.get("is_public") == "on" else 0
|
||||||
if config.config_kobo_sync:
|
if config.config_kobo_sync:
|
||||||
@ -327,24 +330,24 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
|||||||
shelf.user_id = int(current_user.id)
|
shelf.user_id = int(current_user.id)
|
||||||
ub.session.add(shelf)
|
ub.session.add(shelf)
|
||||||
shelf_action = "created"
|
shelf_action = "created"
|
||||||
flash_text = _(u"Shelf %(title)s created", title=shelf_title)
|
flash_text = _("Shelf %(title)s created", title=shelf_title)
|
||||||
else:
|
else:
|
||||||
shelf_action = "changed"
|
shelf_action = "changed"
|
||||||
flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
|
flash_text = _("Shelf %(title)s changed", title=shelf_title)
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
|
log.info("Shelf {} {}".format(shelf_title, shelf_action))
|
||||||
flash(flash_text, category="success")
|
flash(flash_text, category="success")
|
||||||
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||||
except (OperationalError, InvalidRequestError) as ex:
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
log.error_or_exception("Settings Database error: {}".format(ex))
|
log.error_or_exception("Settings Database error: {}".format(ex))
|
||||||
flash(_(u"Database error: %(error)s.", error=ex.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception(ex)
|
log.error_or_exception(ex)
|
||||||
flash(_(u"There was an error"), category="error")
|
flash(_("There was an error"), category="error")
|
||||||
return render_title_template('shelf_edit.html',
|
return render_title_template('shelf_edit.html',
|
||||||
shelf=shelf,
|
shelf=shelf,
|
||||||
title=page_title,
|
title=page_title,
|
||||||
@ -366,7 +369,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
|
|||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
log.error("A public shelf with the name '{}' already exists.".format(title))
|
log.error("A public shelf with the name '{}' already exists.".format(title))
|
||||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
|
flash(_("A public shelf with the name '%(title)s' already exists.", title=title),
|
||||||
category="error")
|
category="error")
|
||||||
else:
|
else:
|
||||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||||
@ -377,7 +380,7 @@ def check_shelf_is_unique(title, is_public, shelf_id=False):
|
|||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
log.error("A private shelf with the name '{}' already exists.".format(title))
|
log.error("A private shelf with the name '{}' already exists.".format(title))
|
||||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
|
flash(_("A private shelf with the name '%(title)s' already exists.", title=title),
|
||||||
category="error")
|
category="error")
|
||||||
return is_shelf_name_unique
|
return is_shelf_name_unique
|
||||||
|
|
||||||
@ -454,14 +457,14 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as e:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error_or_exception("Settings Database error: {}".format(e))
|
log.error_or_exception("Settings Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
|
||||||
|
|
||||||
return render_title_template(page,
|
return render_title_template(page,
|
||||||
entries=result,
|
entries=result,
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
title=_("Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf,
|
shelf=shelf,
|
||||||
page="shelf")
|
page="shelf")
|
||||||
else:
|
else:
|
||||||
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
flash(_("Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
@ -3290,10 +3290,13 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
|||||||
-ms-transform-origin: center top;
|
-ms-transform-origin: center top;
|
||||||
transform-origin: center top;
|
transform-origin: center top;
|
||||||
border: 0;
|
border: 0;
|
||||||
left: 0 !important;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.dropdown-menu:not(.datepicker-dropdown):not(.profileDropli) {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
#add-to-shelves {
|
#add-to-shelves {
|
||||||
|
min-height: 48px;
|
||||||
max-height: calc(100% - 120px);
|
max-height: calc(100% - 120px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@ -4423,38 +4426,6 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
|||||||
left: 49px;
|
left: 49px;
|
||||||
margin-top: 5px
|
margin-top: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after, body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
|
|
||||||
color: hsla(0, 0%, 100%, .7);
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
font-family: plex-icons-new, serif;
|
|
||||||
font-size: 20px;
|
|
||||||
font-stretch: 100%;
|
|
||||||
font-style: normal;
|
|
||||||
font-variant-caps: normal;
|
|
||||||
font-variant-east-asian: normal;
|
|
||||||
font-variant-numeric: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
height: 60px;
|
|
||||||
letter-spacing: normal;
|
|
||||||
line-height: 60px;
|
|
||||||
position: absolute
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:before {
|
|
||||||
content: "\EA30";
|
|
||||||
-webkit-font-variant-ligatures: normal;
|
|
||||||
font-variant-ligatures: normal;
|
|
||||||
left: 20px
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.blur) > .navbar > .container-fluid > .navbar-header:after {
|
|
||||||
content: "\EA2F";
|
|
||||||
-webkit-font-variant-ligatures: normal;
|
|
||||||
font-variant-ligatures: normal;
|
|
||||||
left: 60px
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before {
|
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row:first-of-type > div.col > h2:before, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > h2:first-of-type:before, body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > h1:before {
|
||||||
@ -4842,8 +4813,14 @@ body.advsearch:not(.blur) > div.container-fluid > div.row-fluid > div.col-sm-10
|
|||||||
z-index: 999999999999999999999999999999999999
|
z-index: 999999999999999999999999999999999999
|
||||||
}
|
}
|
||||||
|
|
||||||
.search #shelf-actions, body.login .home-btn {
|
body.search #shelf-actions button#add-to-shelf {
|
||||||
display: none
|
height: 40px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
body.search .discover, body.advsearch .discover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.read:not(.blur) a[href*=readbooks] {
|
body.read:not(.blur) a[href*=readbooks] {
|
||||||
@ -5164,7 +5141,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
|
|||||||
right: 5px
|
right: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
#shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] {
|
body:not(.search) #shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] {
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5181,7 +5158,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
|
|||||||
color: var(--color-primary)
|
color: var(--color-primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
#shelf-actions, #shelf-actions > .btn-group, #shelf-actions > .btn-group > .empty-ul {
|
body:not(.search) #shelf-actions, body:not(.search) #shelf-actions > .btn-group, body:not(.search) #shelf-actions > .btn-group > .empty-ul {
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7309,6 +7286,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
float: right
|
float: right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.blur #main-nav + #scnd-nav .create-shelf, body.blur #main-nav + .col-sm-2 #scnd-nav .create-shelf {
|
||||||
|
float: none;
|
||||||
|
margin: 5px 0 10px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
#main-nav + #scnd-nav .nav-head.hidden-xs {
|
#main-nav + #scnd-nav .nav-head.hidden-xs {
|
||||||
display: list-item !important;
|
display: list-item !important;
|
||||||
width: 225px
|
width: 225px
|
||||||
|
@ -22,3 +22,7 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
|
|||||||
padding: 0 0;
|
padding: 0 0;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.datepicker {color: transparent}
|
||||||
|
input.datepicker:focus {color: transparent}
|
||||||
|
input.datepicker:focus + input {color: #555}
|
||||||
|
@ -149,6 +149,20 @@ body {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mainContent > canvas {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.long-strip > .mainImage {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.long-strip > .mainImage:last-child {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
#titlebar {
|
#titlebar {
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
29
cps/static/css/reader.css
Normal file
29
cps/static/css/reader.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.fontSizeWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0,-50%);
|
||||||
|
width: 90%;
|
||||||
|
height: 60px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0px 15px 40px #7E6D5766;
|
||||||
|
}
|
||||||
|
.slider label {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: Open Sans;
|
||||||
|
padding-right: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.slider input[type="range"] {
|
||||||
|
width: 80%;
|
||||||
|
height: 5px;
|
||||||
|
background: black;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
@ -140,6 +140,7 @@ table .bg-dark-danger a { color: #fff; }
|
|||||||
|
|
||||||
.container-fluid .book {
|
.container-fluid .book {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
max-width: 180px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -433,3 +434,7 @@ div.log {
|
|||||||
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
|
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
|
||||||
#detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; }
|
#detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; }
|
||||||
#detailcover:fullscreen { cursor:zoom-out; border: 0; }
|
#detailcover:fullscreen { cursor:zoom-out; border: 0; }
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
// Move advanced search to side-menu
|
// Move advanced search to side-menu
|
||||||
$("a[href*='advanced']").parent().insertAfter("#nav_new");
|
$("a[href*='advanced']").parent().insertAfter("#nav_new");
|
||||||
$("body").addClass("blur");
|
|
||||||
$("body.stat").addClass("stats");
|
$("body.stat").addClass("stats");
|
||||||
$("body.config").addClass("admin");
|
$("body.config").addClass("admin");
|
||||||
$("body.uiconfig").addClass("admin");
|
$("body.uiconfig").addClass("admin");
|
||||||
@ -29,8 +28,8 @@ $("body > div.container-fluid > div > div.col-sm-10 > div.filterheader").attr("s
|
|||||||
// Back button
|
// Back button
|
||||||
curHref = window.location.href.split("/");
|
curHref = window.location.href.split("/");
|
||||||
prevHref = document.referrer.split("/");
|
prevHref = document.referrer.split("/");
|
||||||
$(".navbar-form.navbar-left")
|
$(".plexBack a").attr('href', encodeURI(document.referrer));
|
||||||
.before('<div class="plexBack"><a href="' + encodeURI(document.referrer) + '"></a></div>');
|
|
||||||
if (history.length === 1 ||
|
if (history.length === 1 ||
|
||||||
curHref[0] +
|
curHref[0] +
|
||||||
curHref[1] +
|
curHref[1] +
|
||||||
@ -44,14 +43,9 @@ if (history.length === 1 ||
|
|||||||
|
|
||||||
//Weird missing a after pressing back from edit.
|
//Weird missing a after pressing back from edit.
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if ($(".plexBack a").length < 1) {
|
$(".plexBack a").attr('href', encodeURI(document.referrer));
|
||||||
$(".plexBack").append('<a href="' + encodeURI(document.referrer) + '"></a>');
|
|
||||||
}
|
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
// Home button
|
|
||||||
$(".plexBack").before('<div class="home-btn"></div>');
|
|
||||||
$("a.navbar-brand").clone().appendTo(".home-btn").empty().removeClass("navbar-brand");
|
|
||||||
/////////////////////////////////
|
/////////////////////////////////
|
||||||
// Start of Book Details Work //
|
// Start of Book Details Work //
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
@ -320,19 +314,11 @@ $(document).mouseup(function (e) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Split path name to array and remove blanks
|
|
||||||
url = window.location.pathname
|
|
||||||
|
|
||||||
// Move create shelf
|
// Move create shelf
|
||||||
$("#nav_createshelf").prependTo(".your-shelves");
|
$("#nav_createshelf").prependTo(".your-shelves");
|
||||||
|
|
||||||
// Create drop-down for profile and move elements to it
|
// Move About link it the profile dropdown
|
||||||
$("#main-nav")
|
$(".profileDropli #top_user").parent().after($("#nav_about").addClass("dropdown"))
|
||||||
.prepend('<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>');
|
|
||||||
$("#top_user").parent().addClass("dropdown").appendTo(".profileDropli");
|
|
||||||
$("#nav_about").addClass("dropdown").appendTo(".profileDropli");
|
|
||||||
$("#register").parent().addClass("dropdown").appendTo(".profileDropli");
|
|
||||||
$("#logout").parent().addClass("dropdown").appendTo(".profileDropli");
|
|
||||||
|
|
||||||
// Remove the modals except from some areas where they are needed
|
// Remove the modals except from some areas where they are needed
|
||||||
bodyClass = $("body").attr("class").split(" ");
|
bodyClass = $("body").attr("class").split(" ");
|
||||||
@ -371,31 +357,6 @@ $(document).on("click", ".dropdown-toggle", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fade out content on page unload
|
|
||||||
// delegate all clicks on "a" tag (links)
|
|
||||||
/*$(document).on("click", "a:not(.btn-toolbar a, a[href*='shelf/remove'], .identifiers a, .bookinfo , .btn-group > a, #add-to-shelves a, #book-list a, .stat.blur a )", function () {
|
|
||||||
|
|
||||||
// get the href attribute
|
|
||||||
var newUrl = $(this).attr("href");
|
|
||||||
|
|
||||||
// veryfy if the new url exists or is a hash
|
|
||||||
if (!newUrl || newUrl[0] === "#") {
|
|
||||||
// set that hash
|
|
||||||
location.hash = newUrl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
now, fadeout the html (whole page)
|
|
||||||
$( '.blur-wrapper' ).fadeOut(250);
|
|
||||||
$(".row-fluid .col-sm-10").fadeOut(500,function () {
|
|
||||||
// when the animation is complete, set the new location
|
|
||||||
location = newUrl;
|
|
||||||
});
|
|
||||||
|
|
||||||
// prevent the default browser behavior.
|
|
||||||
return false;
|
|
||||||
});*/
|
|
||||||
|
|
||||||
// Collapse long text into read-more
|
// Collapse long text into read-more
|
||||||
$("div.comments").readmore({
|
$("div.comments").readmore({
|
||||||
collapsedHeight: 134,
|
collapsedHeight: 134,
|
||||||
@ -408,6 +369,13 @@ $("div.comments").readmore({
|
|||||||
// End of Global Work //
|
// End of Global Work //
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
|
|
||||||
|
// Search Results
|
||||||
|
if($("body.search").length > 0) {
|
||||||
|
$('div[aria-label="Add to shelves"]').click(function () {
|
||||||
|
$("#add-to-shelves").toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Advanced Search Results
|
// Advanced Search Results
|
||||||
if($("body.advsearch").length > 0) {
|
if($("body.advsearch").length > 0) {
|
||||||
$("#loader + .container-fluid")
|
$("#loader + .container-fluid")
|
||||||
@ -458,6 +426,8 @@ if ($("body.author").length > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Split path name to array and remove blanks
|
||||||
|
url = window.location.pathname
|
||||||
// Ereader Page - add class to iframe body on ereader page after it loads.
|
// Ereader Page - add class to iframe body on ereader page after it loads.
|
||||||
backurl = "../../book/" + url[2]
|
backurl = "../../book/" + url[2]
|
||||||
$("body.epub #title-controls")
|
$("body.epub #title-controls")
|
||||||
@ -540,6 +510,7 @@ if ($("body.shelf").length > 0) {
|
|||||||
// Rest of Tooltips
|
// Rest of Tooltips
|
||||||
$(".home-btn > a").attr({
|
$(".home-btn > a").attr({
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
|
"href": $(".navbar-brand")[0].href,
|
||||||
"title": $(document.body).attr("data-text"), // Home
|
"title": $(document.body).attr("data-text"), // Home
|
||||||
"data-placement": "bottom"
|
"data-placement": "bottom"
|
||||||
})
|
})
|
||||||
@ -666,7 +637,7 @@ $("#sendbtn").attr({
|
|||||||
|
|
||||||
$("#sendbtn2").attr({
|
$("#sendbtn2").attr({
|
||||||
"data-toggle-two": "tooltip",
|
"data-toggle-two": "tooltip",
|
||||||
"title": $("#sendbtn2").text(), // "Send to E-Reader",
|
"title": $("#sendbtn2").text(), // "Send to eReader",
|
||||||
"data-placement": "bottom",
|
"data-placement": "bottom",
|
||||||
"data-viewport": ".btn-toolbar"
|
"data-viewport": ".btn-toolbar"
|
||||||
})
|
})
|
||||||
|
File diff suppressed because it is too large
Load Diff
14
cps/static/js/compress/jszip.min.js
vendored
Normal file
14
cps/static/js/compress/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
cps/static/js/compress/jszip_epub.min.js
vendored
Normal file
13
cps/static/js/compress/jszip_epub.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -125,7 +125,7 @@ function loadArchiveFormats(formats, cb) {
|
|||||||
_loaded_archive_formats.push(archive_format);
|
_loaded_archive_formats.push(archive_format);
|
||||||
break;
|
break;
|
||||||
case 'zip':
|
case 'zip':
|
||||||
loadScript(path + 'jszip.js', checkForLoadDone);
|
loadScript(path + 'jszip.min.js', checkForLoadDone);
|
||||||
_loaded_archive_formats.push(archive_format);
|
_loaded_archive_formats.push(archive_format);
|
||||||
break;
|
break;
|
||||||
case 'tar':
|
case 'tar':
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
* Copyright (C) 2018 jkrehm
|
* Copyright (C) 2018-2023 jkrehm, OzzieIsaacs
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -17,6 +17,36 @@
|
|||||||
|
|
||||||
/* global _ */
|
/* global _ */
|
||||||
|
|
||||||
|
function handleResponse (data) {
|
||||||
|
$(".row-fluid.text-center").remove();
|
||||||
|
$("#flash_danger").remove();
|
||||||
|
$("#flash_success").remove();
|
||||||
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
|
if($("#bookDetailsModal").is(":visible")) {
|
||||||
|
data.forEach(function (item) {
|
||||||
|
$(".modal-header").after('<div id="flash_' + item.type +
|
||||||
|
'" class="text-center alert alert-' + item.type + '">' + item.message + '</div>');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
data.forEach(function (item) {
|
||||||
|
$(".navbar").after('<div class="row-fluid text-center">' +
|
||||||
|
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
||||||
|
'</div>');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(".sendbtn-form").click(function() {
|
||||||
|
$.ajax({
|
||||||
|
method: 'post',
|
||||||
|
url: $(this).data('href'),
|
||||||
|
data: {csrf_token: $("input[name='csrf_token']").val()},
|
||||||
|
success: function (data) {
|
||||||
|
handleResponse(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$("#have_read_form").ajaxForm();
|
$("#have_read_form").ajaxForm();
|
||||||
});
|
});
|
||||||
|
@ -62,6 +62,7 @@ var currentImage = 0;
|
|||||||
var imageFiles = [];
|
var imageFiles = [];
|
||||||
var imageFilenames = [];
|
var imageFilenames = [];
|
||||||
var totalImages = 0;
|
var totalImages = 0;
|
||||||
|
var prevScrollPosition = 0;
|
||||||
|
|
||||||
var settings = {
|
var settings = {
|
||||||
hflip: false,
|
hflip: false,
|
||||||
@ -71,7 +72,8 @@ var settings = {
|
|||||||
theme: "light",
|
theme: "light",
|
||||||
direction: 0, // 0 = Left to Right, 1 = Right to Left
|
direction: 0, // 0 = Left to Right, 1 = Right to Left
|
||||||
nextPage: 0, // 0 = Reset to Top, 1 = Remember Position
|
nextPage: 0, // 0 = Reset to Top, 1 = Remember Position
|
||||||
scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar
|
scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
|
||||||
|
pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
|
||||||
};
|
};
|
||||||
|
|
||||||
kthoom.saveSettings = function() {
|
kthoom.saveSettings = function() {
|
||||||
@ -176,18 +178,38 @@ kthoom.ImageFile = function(file) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateDirectionButtons(){
|
||||||
|
var left, right = 1;
|
||||||
|
if (currentImage == 0 ) {
|
||||||
|
if (settings.direction === 0) {
|
||||||
|
left = 0;
|
||||||
|
} else {
|
||||||
|
right = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((currentImage + 1) >= Math.max(totalImages, imageFiles.length)) {
|
||||||
|
if (settings.direction === 0) {
|
||||||
|
right = 0;
|
||||||
|
} else {
|
||||||
|
left = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left === 1 ? $("#left").show() : $("#left").hide();
|
||||||
|
right === 1 ? $("#right").show() : $("#right").hide();
|
||||||
|
}
|
||||||
function initProgressClick() {
|
function initProgressClick() {
|
||||||
$("#progress").click(function(e) {
|
$("#progress").click(function(e) {
|
||||||
var offset = $(this).offset();
|
var offset = $(this).offset();
|
||||||
var x = e.pageX - offset.left;
|
var x = e.pageX - offset.left;
|
||||||
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
|
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
|
||||||
currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1;
|
currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1;
|
||||||
|
updateDirectionButtons();
|
||||||
|
setBookmark();
|
||||||
updatePage();
|
updatePage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFromArrayBuffer(ab) {
|
function loadFromArrayBuffer(ab) {
|
||||||
var lastCompletion = 0;
|
|
||||||
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
||||||
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
|
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
|
||||||
// Open the file as an archive
|
// Open the file as an archive
|
||||||
@ -216,9 +238,14 @@ function loadFromArrayBuffer(ab) {
|
|||||||
"</a>" +
|
"</a>" +
|
||||||
"</li>"
|
"</li>"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drawCanvas();
|
||||||
|
setImage(test.dataURI, null);
|
||||||
|
|
||||||
// display first page if we haven't yet
|
// display first page if we haven't yet
|
||||||
if (imageFiles.length === currentImage + 1) {
|
if (imageFiles.length === currentImage + 1) {
|
||||||
updatePage(lastCompletion);
|
updateDirectionButtons();
|
||||||
|
updatePage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
totalImages--;
|
totalImages--;
|
||||||
@ -233,13 +260,6 @@ function loadFromArrayBuffer(ab) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrollTocToActive() {
|
function scrollTocToActive() {
|
||||||
// Scroll to the thumbnail in the TOC on page change
|
|
||||||
$("#tocView").stop().animate({
|
|
||||||
scrollTop: $("#tocView a.active").position().top
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePage() {
|
|
||||||
$(".page").text((currentImage + 1 ) + "/" + totalImages);
|
$(".page").text((currentImage + 1 ) + "/" + totalImages);
|
||||||
|
|
||||||
// Mark the current page in the TOC
|
// Mark the current page in the TOC
|
||||||
@ -251,22 +271,40 @@ function updatePage() {
|
|||||||
// Set it to active
|
// Set it to active
|
||||||
.addClass("active");
|
.addClass("active");
|
||||||
|
|
||||||
scrollTocToActive();
|
// Scroll to the thumbnail in the TOC on page change
|
||||||
updateProgress();
|
$("#tocView").stop().animate({
|
||||||
|
scrollTop: $("#tocView a.active").position().top
|
||||||
if (imageFiles[currentImage]) {
|
}, 200);
|
||||||
setImage(imageFiles[currentImage].dataURI);
|
|
||||||
} else {
|
|
||||||
setImage("loading");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("body").toggleClass("dark-theme", settings.theme === "dark");
|
function updatePage() {
|
||||||
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
|
scrollTocToActive();
|
||||||
|
scrollCurrentImageIntoView();
|
||||||
|
updateProgress();
|
||||||
|
pageDisplayUpdate();
|
||||||
|
setTheme();
|
||||||
|
|
||||||
kthoom.setSettings();
|
kthoom.setSettings();
|
||||||
kthoom.saveSettings();
|
kthoom.saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTheme() {
|
||||||
|
$("body").toggleClass("dark-theme", settings.theme === "dark");
|
||||||
|
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageDisplayUpdate() {
|
||||||
|
if(settings.pageDisplay === 0) {
|
||||||
|
$(".mainImage").addClass("hide");
|
||||||
|
$(".mainImage").eq(currentImage).removeClass("hide");
|
||||||
|
$("#mainContent").removeClass("long-strip");
|
||||||
|
} else {
|
||||||
|
$(".mainImage").removeClass("hide");
|
||||||
|
$("#mainContent").addClass("long-strip");
|
||||||
|
scrollCurrentImageIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateProgress(loadPercentage) {
|
function updateProgress(loadPercentage) {
|
||||||
if (settings.direction === 0) {
|
if (settings.direction === 0) {
|
||||||
$("#progress .bar-read")
|
$("#progress .bar-read")
|
||||||
@ -298,29 +336,19 @@ function updateProgress(loadPercentage) {
|
|||||||
$("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"});
|
$("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setImage(url) {
|
function setImage(url, _canvas) {
|
||||||
var canvas = $("#mainImage")[0];
|
var canvas = _canvas || $(".mainImage").slice(-1)[0]; // Select the last item on the array if _canvas is null
|
||||||
var x = $("#mainImage")[0].getContext("2d");
|
var x = canvas.getContext("2d");
|
||||||
|
|
||||||
$("#mainText").hide();
|
$("#mainText").hide();
|
||||||
if (url === "loading") {
|
|
||||||
updateScale(true);
|
|
||||||
canvas.width = innerWidth - 100;
|
|
||||||
canvas.height = 200;
|
|
||||||
x.fillStyle = "black";
|
|
||||||
x.textAlign = "center";
|
|
||||||
x.font = "24px sans-serif";
|
|
||||||
x.strokeStyle = "black";
|
|
||||||
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100);
|
|
||||||
} else {
|
|
||||||
if (url === "error") {
|
if (url === "error") {
|
||||||
updateScale(true);
|
|
||||||
canvas.width = innerWidth - 100;
|
|
||||||
canvas.height = 200;
|
|
||||||
x.fillStyle = "black";
|
x.fillStyle = "black";
|
||||||
x.textAlign = "center";
|
x.textAlign = "center";
|
||||||
x.font = "24px sans-serif";
|
x.font = "24px sans-serif";
|
||||||
x.strokeStyle = "black";
|
x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
|
||||||
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
|
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
|
||||||
|
|
||||||
|
$(".mainImage").slice(-1).addClass("error");
|
||||||
} else {
|
} else {
|
||||||
if ($("body").css("scrollHeight") / innerHeight > 1) {
|
if ($("body").css("scrollHeight") / innerHeight > 1) {
|
||||||
$("body").css("overflowY", "scroll");
|
$("body").css("overflowY", "scroll");
|
||||||
@ -330,7 +358,6 @@ function setImage(url) {
|
|||||||
img.onerror = function() {
|
img.onerror = function() {
|
||||||
canvas.width = innerWidth - 100;
|
canvas.width = innerWidth - 100;
|
||||||
canvas.height = 300;
|
canvas.height = 300;
|
||||||
updateScale(true);
|
|
||||||
x.fillStyle = "black";
|
x.fillStyle = "black";
|
||||||
x.font = "50px sans-serif";
|
x.font = "50px sans-serif";
|
||||||
x.strokeStyle = "black";
|
x.strokeStyle = "black";
|
||||||
@ -384,8 +411,6 @@ function setImage(url) {
|
|||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
x.drawImage(img, 0, 0);
|
x.drawImage(img, 0, 0);
|
||||||
|
|
||||||
updateScale(false);
|
|
||||||
|
|
||||||
canvas.style.display = "";
|
canvas.style.display = "";
|
||||||
$("body").css("overflowY", "");
|
$("body").css("overflowY", "");
|
||||||
x.restore();
|
x.restore();
|
||||||
@ -393,6 +418,12 @@ function setImage(url) {
|
|||||||
img.src = url;
|
img.src = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reloadImages is a slow process when multiple images are involved. Only used when rotating/mirroring
|
||||||
|
function reloadImages() {
|
||||||
|
for(i=0; i < imageFiles.length; i++) {
|
||||||
|
setImage(imageFiles[i].dataURI, $(".mainImage")[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLeftPage() {
|
function showLeftPage() {
|
||||||
@ -401,6 +432,7 @@ function showLeftPage() {
|
|||||||
} else {
|
} else {
|
||||||
showNextPage();
|
showNextPage();
|
||||||
}
|
}
|
||||||
|
setBookmark();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRightPage() {
|
function showRightPage() {
|
||||||
@ -409,6 +441,7 @@ function showRightPage() {
|
|||||||
} else {
|
} else {
|
||||||
showPrevPage();
|
showPrevPage();
|
||||||
}
|
}
|
||||||
|
setBookmark();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPrevPage() {
|
function showPrevPage() {
|
||||||
@ -418,10 +451,8 @@ function showPrevPage() {
|
|||||||
currentImage++;
|
currentImage++;
|
||||||
} else {
|
} else {
|
||||||
updatePage();
|
updatePage();
|
||||||
if (settings.nextPage === 0) {
|
|
||||||
$("#mainContent").scrollTop(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
updateDirectionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNextPage() {
|
function showNextPage() {
|
||||||
@ -431,36 +462,54 @@ function showNextPage() {
|
|||||||
currentImage--;
|
currentImage--;
|
||||||
} else {
|
} else {
|
||||||
updatePage();
|
updatePage();
|
||||||
if (settings.nextPage === 0) {
|
|
||||||
$("#mainContent").scrollTop(0);
|
|
||||||
}
|
}
|
||||||
|
updateDirectionButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollCurrentImageIntoView() {
|
||||||
|
if(settings.pageDisplay == 0) {
|
||||||
|
// This will scroll all the way up when Single Page is selected
|
||||||
|
$("#mainContent").scrollTop(0);
|
||||||
|
} else {
|
||||||
|
// This will scroll to the image when Long Strip is selected
|
||||||
|
$("#mainContent").stop().animate({
|
||||||
|
scrollTop: $(".mainImage").eq(currentImage).offset().top + $("#mainContent").scrollTop() - $("#mainContent").offset().top
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateScale(clear) {
|
function updateScale() {
|
||||||
var mainImageStyle = getElem("mainImage").style;
|
var canvasArray = $("#mainContent > canvas");
|
||||||
mainImageStyle.width = "";
|
|
||||||
mainImageStyle.height = "";
|
|
||||||
mainImageStyle.maxWidth = "";
|
|
||||||
mainImageStyle.maxHeight = "";
|
|
||||||
var maxheight = innerHeight - 50;
|
var maxheight = innerHeight - 50;
|
||||||
|
|
||||||
if (!clear) {
|
canvasArray.css("width", "");
|
||||||
|
canvasArray.css("height", "");
|
||||||
|
canvasArray.css("maxWidth", "");
|
||||||
|
canvasArray.css("maxHeight", "");
|
||||||
|
|
||||||
|
if(settings.pageDisplay === 0) {
|
||||||
|
canvasArray.addClass("hide");
|
||||||
|
pageDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
switch (settings.fitMode) {
|
switch (settings.fitMode) {
|
||||||
case kthoom.Key.B:
|
case kthoom.Key.B:
|
||||||
mainImageStyle.maxWidth = "100%";
|
canvasArray.css("maxWidth", "100%");
|
||||||
mainImageStyle.maxHeight = maxheight + "px";
|
canvasArray.css("maxHeight", maxheight + "px");
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.H:
|
case kthoom.Key.H:
|
||||||
mainImageStyle.height = maxheight + "px";
|
canvasArray.css("maxHeight", maxheight + "px");
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.W:
|
case kthoom.Key.W:
|
||||||
mainImageStyle.width = "100%";
|
canvasArray.css("width", "100%");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
$("#mainContent > canvas.error").css("width", innerWidth - 100);
|
||||||
|
$("#mainContent > canvas.error").css("height", 200);
|
||||||
|
|
||||||
$("#mainContent").css({maxHeight: maxheight + 5});
|
$("#mainContent").css({maxHeight: maxheight + 5});
|
||||||
kthoom.setSettings();
|
kthoom.setSettings();
|
||||||
kthoom.saveSettings();
|
kthoom.saveSettings();
|
||||||
@ -477,6 +526,20 @@ function keyHandler(evt) {
|
|||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
showRightPage();
|
showRightPage();
|
||||||
break;
|
break;
|
||||||
|
case kthoom.Key.S:
|
||||||
|
if (hasModifier) break;
|
||||||
|
settings.pageDisplay = 0;
|
||||||
|
pageDisplayUpdate();
|
||||||
|
kthoom.setSettings();
|
||||||
|
kthoom.saveSettings();
|
||||||
|
break;
|
||||||
|
case kthoom.Key.O:
|
||||||
|
if (hasModifier) break;
|
||||||
|
settings.pageDisplay = 1;
|
||||||
|
pageDisplayUpdate();
|
||||||
|
kthoom.setSettings();
|
||||||
|
kthoom.saveSettings();
|
||||||
|
break;
|
||||||
case kthoom.Key.L:
|
case kthoom.Key.L:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
settings.rotateTimes--;
|
settings.rotateTimes--;
|
||||||
@ -484,6 +547,7 @@ function keyHandler(evt) {
|
|||||||
settings.rotateTimes = 3;
|
settings.rotateTimes = 3;
|
||||||
}
|
}
|
||||||
updatePage();
|
updatePage();
|
||||||
|
reloadImages();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.R:
|
case kthoom.Key.R:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
@ -492,6 +556,7 @@ function keyHandler(evt) {
|
|||||||
settings.rotateTimes = 0;
|
settings.rotateTimes = 0;
|
||||||
}
|
}
|
||||||
updatePage();
|
updatePage();
|
||||||
|
reloadImages();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.F:
|
case kthoom.Key.F:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
@ -507,26 +572,27 @@ function keyHandler(evt) {
|
|||||||
settings.hflip = true;
|
settings.hflip = true;
|
||||||
}
|
}
|
||||||
updatePage();
|
updatePage();
|
||||||
|
reloadImages();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.W:
|
case kthoom.Key.W:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
settings.fitMode = kthoom.Key.W;
|
settings.fitMode = kthoom.Key.W;
|
||||||
updateScale(false);
|
updateScale();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.H:
|
case kthoom.Key.H:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
settings.fitMode = kthoom.Key.H;
|
settings.fitMode = kthoom.Key.H;
|
||||||
updateScale(false);
|
updateScale();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.B:
|
case kthoom.Key.B:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
settings.fitMode = kthoom.Key.B;
|
settings.fitMode = kthoom.Key.B;
|
||||||
updateScale(false);
|
updateScale();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.N:
|
case kthoom.Key.N:
|
||||||
if (hasModifier) break;
|
if (hasModifier) break;
|
||||||
settings.fitMode = kthoom.Key.N;
|
settings.fitMode = kthoom.Key.N;
|
||||||
updateScale(false);
|
updateScale();
|
||||||
break;
|
break;
|
||||||
case kthoom.Key.SPACE:
|
case kthoom.Key.SPACE:
|
||||||
if (evt.shiftKey) {
|
if (evt.shiftKey) {
|
||||||
@ -545,6 +611,53 @@ function keyHandler(evt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawCanvas() {
|
||||||
|
var maxheight = innerHeight - 50;
|
||||||
|
var canvasElement = $("<canvas></canvas>");
|
||||||
|
var x = canvasElement[0].getContext("2d");
|
||||||
|
canvasElement.addClass("mainImage");
|
||||||
|
|
||||||
|
switch (settings.fitMode) {
|
||||||
|
case kthoom.Key.B:
|
||||||
|
canvasElement.css("maxWidth", "100%");
|
||||||
|
canvasElement.css("maxHeight", maxheight + "px");
|
||||||
|
break;
|
||||||
|
case kthoom.Key.H:
|
||||||
|
canvasElement.css("maxHeight", maxheight + "px");
|
||||||
|
break;
|
||||||
|
case kthoom.Key.W:
|
||||||
|
canvasElement.css("width", "100%");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(settings.pageDisplay === 0) {
|
||||||
|
canvasElement.addClass("hide");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fill with Placeholder text. setImage will override this
|
||||||
|
canvasElement.width = innerWidth - 100;
|
||||||
|
canvasElement.height = 200;
|
||||||
|
x.fillStyle = "black";
|
||||||
|
x.textAlign = "center";
|
||||||
|
x.font = "24px sans-serif";
|
||||||
|
x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
|
||||||
|
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100);
|
||||||
|
|
||||||
|
$("#mainContent").append(canvasElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateArrows() {
|
||||||
|
if ($('input[name="direction"]:checked').val() === "0") {
|
||||||
|
$("#prev_page_key").html("←");
|
||||||
|
$("#next_page_key").html("→");
|
||||||
|
} else {
|
||||||
|
$("#prev_page_key").html("→");
|
||||||
|
$("#next_page_key").html("←");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function init(filename) {
|
function init(filename) {
|
||||||
var request = new XMLHttpRequest();
|
var request = new XMLHttpRequest();
|
||||||
request.open("GET", filename);
|
request.open("GET", filename);
|
||||||
@ -556,16 +669,17 @@ function init(filename) {
|
|||||||
console.warn(request.statusText, request.responseText);
|
console.warn(request.statusText, request.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
kthoom.loadSettings();
|
||||||
|
setTheme();
|
||||||
|
updateScale();
|
||||||
request.send();
|
request.send();
|
||||||
initProgressClick();
|
initProgressClick();
|
||||||
document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : "";
|
document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : "";
|
||||||
kthoom.loadSettings();
|
|
||||||
updateScale(true);
|
|
||||||
|
|
||||||
$(document).keydown(keyHandler);
|
$(document).keydown(keyHandler);
|
||||||
|
|
||||||
$(window).resize(function () {
|
$(window).resize(function () {
|
||||||
updateScale(false);
|
updateScale();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open TOC menu
|
// Open TOC menu
|
||||||
@ -596,28 +710,36 @@ function init(filename) {
|
|||||||
value = /^\d+$/.test(value) ? parseInt(value) : value;
|
value = /^\d+$/.test(value) ? parseInt(value) : value;
|
||||||
|
|
||||||
settings[this.name] = value;
|
settings[this.name] = value;
|
||||||
|
|
||||||
|
if (["hflip", "vflip", "rotateTimes"].includes(this.name)) {
|
||||||
|
reloadImages();
|
||||||
|
} else if (this.name === "direction") {
|
||||||
|
updateDirectionButtons();
|
||||||
|
return updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
updatePage();
|
updatePage();
|
||||||
updateScale(false);
|
updateScale();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
$(".closer, .overlay").click(function () {
|
$(".closer, .overlay").click(function () {
|
||||||
$(".md-show").removeClass("md-show");
|
$(".md-show").removeClass("md-show");
|
||||||
|
$("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it
|
||||||
});
|
});
|
||||||
|
|
||||||
// TOC thumbnail pagination
|
// TOC thumbnail pagination
|
||||||
$("#thumbnails").on("click", "a", function () {
|
$("#thumbnails").on("click", "a", function () {
|
||||||
currentImage = $(this).data("page") - 1;
|
currentImage = $(this).data("page") - 1;
|
||||||
updatePage();
|
updatePage();
|
||||||
if (settings.nextPage === 0) {
|
|
||||||
$("#mainContent").scrollTop(0);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fullscreen mode
|
// Fullscreen mode
|
||||||
if (typeof screenfull !== "undefined") {
|
if (typeof screenfull !== "undefined") {
|
||||||
$("#fullscreen").click(function () {
|
$("#fullscreen").click(function () {
|
||||||
screenfull.toggle($("#container")[0]);
|
screenfull.toggle($("#container")[0]);
|
||||||
|
// Focus on main container so you can use up/down keys immediately after fullscreen
|
||||||
|
$("#mainContent").focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (screenfull.raw) {
|
if (screenfull.raw) {
|
||||||
@ -641,8 +763,8 @@ function init(filename) {
|
|||||||
showRightPage();
|
showRightPage();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
$("#mainImage").click(function(evt) {
|
$(".mainImage").click(function (evt) {
|
||||||
// Firefox does not support offsetX/Y so we have to manually calculate
|
// Firefox does not support offsetX/Y, so we have to manually calculate
|
||||||
// where the user clicked in the image.
|
// where the user clicked in the image.
|
||||||
var mainContentWidth = $("#mainContent").width();
|
var mainContentWidth = $("#mainContent").width();
|
||||||
var mainContentHeight = $("#mainContent").height();
|
var mainContentHeight = $("#mainContent").height();
|
||||||
@ -676,5 +798,73 @@ function init(filename) {
|
|||||||
showRightPage();
|
showRightPage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
|
||||||
|
$("#mainContent").scroll(function (){
|
||||||
|
var scroll = $("#mainContent").scrollTop();
|
||||||
|
var viewLength = 0;
|
||||||
|
$(".mainImage").each(function(){
|
||||||
|
viewLength += $(this).height();
|
||||||
|
});
|
||||||
|
if (settings.pageDisplay === 0) {
|
||||||
|
// Don't trigger the scroll for Single Page
|
||||||
|
} else if (scroll > prevScrollPosition) {
|
||||||
|
//Scroll Down
|
||||||
|
if (currentImage + 1 < imageFiles.length) {
|
||||||
|
if (currentImageOffset(currentImage + 1) <= 1) {
|
||||||
|
currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
|
||||||
|
if ( currentImage >= imageFiles.length) {
|
||||||
|
currentImage = imageFiles.length - 1;
|
||||||
|
}
|
||||||
|
console.log(currentImage);
|
||||||
|
scrollTocToActive();
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Scroll Up
|
||||||
|
if (currentImage - 1 > -1) {
|
||||||
|
if (currentImageOffset(currentImage - 1) >= 0) {
|
||||||
|
currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
|
||||||
|
console.log(currentImage);
|
||||||
|
scrollTocToActive();
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update scroll position
|
||||||
|
prevScrollPosition = scroll;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentImageOffset(imageIndex) {
|
||||||
|
return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBookmark() {
|
||||||
|
// get csrf_token
|
||||||
|
let csrf_token = $("input[name='csrf_token']").val();
|
||||||
|
//This sends a bookmark update to calibreweb.
|
||||||
|
$.ajax(calibre.bookmarkUrl, {
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
csrf_token: csrf_token,
|
||||||
|
bookmark: currentImage
|
||||||
|
}
|
||||||
|
}).fail(function (xhr, status, error) {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('input[name="direction"]').change(function () {
|
||||||
|
updateArrows();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#left').click(function () {
|
||||||
|
showLeftPage();
|
||||||
|
});
|
||||||
|
$('#right').click(function () {
|
||||||
|
showRightPage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
4
cps/static/js/libs/Sortable.min.js
vendored
4
cps/static/js/libs/Sortable.min.js
vendored
File diff suppressed because one or more lines are too long
8
cps/static/js/libs/bar-ui.js
vendored
8
cps/static/js/libs/bar-ui.js
vendored
@ -177,6 +177,9 @@
|
|||||||
|
|
||||||
whileplaying: function () {
|
whileplaying: function () {
|
||||||
|
|
||||||
|
// get csrf_token
|
||||||
|
let csrf_token = $("input[name='csrf_token']").val();
|
||||||
|
|
||||||
|
|
||||||
//This sends a bookmark update to calibreweb every 30 seconds.
|
//This sends a bookmark update to calibreweb every 30 seconds.
|
||||||
if (this.progressBuffer == undefined) {
|
if (this.progressBuffer == undefined) {
|
||||||
@ -187,7 +190,10 @@
|
|||||||
|
|
||||||
$.ajax(calibre.bookmarkUrl, {
|
$.ajax(calibre.bookmarkUrl, {
|
||||||
method: "post",
|
method: "post",
|
||||||
data: { bookmark: this.position }
|
data: {
|
||||||
|
csrf_token: csrf_token,
|
||||||
|
bookmark: this.position
|
||||||
|
}
|
||||||
}).fail(function (xhr, status, error) {
|
}).fail(function (xhr, status, error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery);
|
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(a){a.fn.datepicker.dates.no={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthsShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],today:"i dag",monthsTitle:"Måneder",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery);
|
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(a){a.fn.datepicker.dates.pt={days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"],daysShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],daysMin:["Do","Se","Te","Qu","Qu","Se","Sa"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthsShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],today:"Hoje",monthsTitle:"Meses",clear:"Limpar",format:"dd/mm/yyyy"}}(jQuery);
|
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(a){a.fn.datepicker.dates.sk={days:["Nedeľa","Pondelok","Utorok","Streda","Štvrtok","Piatok","Sobota"],daysShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],daysMin:["Ne","Po","Ut","St","Št","Pia","So"],months:["Január","Február","Marec","Apríl","Máj","Jún","Júl","August","September","Október","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],today:"Dnes",clear:"Vymazať",weekStart:1,format:"d.m.yyyy"}}(jQuery);
|
2
cps/static/js/libs/epub.min.js
vendored
2
cps/static/js/libs/epub.min.js
vendored
File diff suppressed because one or more lines are too long
4
cps/static/js/libs/jquery.min.js
vendored
4
cps/static/js/libs/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
2
cps/static/js/libs/jquery.min.map
vendored
2
cps/static/js/libs/jquery.min.map
vendored
File diff suppressed because one or more lines are too long
13
cps/static/js/libs/jszip.min.js
vendored
13
cps/static/js/libs/jszip.min.js
vendored
File diff suppressed because one or more lines are too long
1
cps/static/js/libs/pwstrength/i18next.min.js
vendored
Normal file
1
cps/static/js/libs/pwstrength/i18next.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cps/static/js/libs/pwstrength/i18nextHttpBackend.min.js
vendored
Normal file
1
cps/static/js/libs/pwstrength/i18nextHttpBackend.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
17
cps/static/js/libs/pwstrength/locales/ar.json
Normal file
17
cps/static/js/libs/pwstrength/locales/ar.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "كلمة المرور قصيرة جداً",
|
||||||
|
"wordMaxLength": "كلمة المرور طويلة جدا",
|
||||||
|
"wordInvalidChar": "تحتوي كلمة المرور على رموز غير صالحة",
|
||||||
|
"wordNotEmail": "لا تستخدم بريدك الإلكتروني ككلمة مرور",
|
||||||
|
"wordSimilarToUsername": "لا يمكن ان تحتوي كلمة المرور على إسم المستخدم",
|
||||||
|
"wordTwoCharacterClasses": "إستخدم فئات أحرف مختلفة",
|
||||||
|
"wordRepetitions": "تكرارات كثيرة",
|
||||||
|
"wordSequences": "تحتوي كلمة المرور على أنماط متتابعة",
|
||||||
|
"errorList": "الأخطاء:",
|
||||||
|
"veryWeak": "ضعيفة جداً",
|
||||||
|
"weak": "ضعيفة",
|
||||||
|
"normal": "عادية",
|
||||||
|
"medium": "متوسطة",
|
||||||
|
"strong": "قوية",
|
||||||
|
"veryStrong": "قوية جداً"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/cs.json
Normal file
17
cps/static/js/libs/pwstrength/locales/cs.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Vaše heslo je příliš krátké",
|
||||||
|
"wordMaxLength": "Vaše heslo je příliš dlouhé",
|
||||||
|
"wordInvalidChar": "Vaše heslo obsahuje neplatný znak",
|
||||||
|
"wordNotEmail": "Nepoužívejte Váš email jako Vaše heslo",
|
||||||
|
"wordSimilarToUsername": "Vaše heslo nesmí obsahovat přihlašovací jméno",
|
||||||
|
"wordTwoCharacterClasses": "Použijte různé druhy znaků",
|
||||||
|
"wordRepetitions": "Příliš mnoho opakování",
|
||||||
|
"wordSequences": "Vaše heslo obsahuje postupnost",
|
||||||
|
"errorList": "Chyby:",
|
||||||
|
"veryWeak": "Velmi slabé",
|
||||||
|
"weak": "Slabé",
|
||||||
|
"normal": "Normální",
|
||||||
|
"medium": "Středně silné",
|
||||||
|
"strong": "Silné",
|
||||||
|
"veryStrong": "Velmi silné"
|
||||||
|
}
|
21
cps/static/js/libs/pwstrength/locales/de.json
Normal file
21
cps/static/js/libs/pwstrength/locales/de.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Das Passwort ist zu kurz",
|
||||||
|
"wordMaxLength": "Das Passwort ist zu lang",
|
||||||
|
"wordInvalidChar": "Das Passwort enthält ein ungültiges Zeichen",
|
||||||
|
"wordNotEmail": "Das Passwort darf die E-Mail Adresse nicht enthalten",
|
||||||
|
"wordSimilarToUsername": "Das Passwort darf den Benutzernamen nicht enthalten",
|
||||||
|
"wordTwoCharacterClasses": "Bitte Buchstaben und Ziffern verwenden",
|
||||||
|
"wordRepetitions": "Zu viele Wiederholungen",
|
||||||
|
"wordSequences": "Das Passwort enthält Buchstabensequenzen",
|
||||||
|
"wordLowercase": "Bitte mindestens einen Kleinbuchstaben verwenden",
|
||||||
|
"wordUppercase": "Bitte mindestens einen Großbuchstaben verwenden",
|
||||||
|
"wordOneNumber": "Bitte mindestens eine Ziffern verwenden",
|
||||||
|
"wordOneSpecialChar": "Bitte mindestens ein Sonderzeichen verwenden",
|
||||||
|
"errorList": "Fehler:",
|
||||||
|
"veryWeak": "Sehr schwach",
|
||||||
|
"weak": "Schwach",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Mittel",
|
||||||
|
"strong": "Stark",
|
||||||
|
"veryStrong": "Sehr stark"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/el.json
Normal file
17
cps/static/js/libs/pwstrength/locales/el.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Ο κωδικός πρόσβασης δεν έχει τον ελάχιστο αριθμό χαρακτήρων",
|
||||||
|
"wordMaxLength": "Ο κωδικός πρόσβασής σας είναι πολύ μεγάλος",
|
||||||
|
"wordInvalidChar": "Ο κωδικός πρόσβασής σας περιέχει έναν μη έγκυρο χαρακτήρα",
|
||||||
|
"wordNotEmail": "Μη χρησιμοποιείτε το email ως κωδικό",
|
||||||
|
"wordSimilarToUsername": "Ο κωδικός πρόσβασης δεν πρέπει να περιέχει το username",
|
||||||
|
"wordTwoCharacterClasses": "Χρησιμοποιήστε διαφορετικές κλάσεις χαρακτήρων",
|
||||||
|
"wordRepetitions": "Πολλές επαναλήψεις",
|
||||||
|
"wordSequences": "Ο κωδικός πρόσβασης περιέχει επαναλήψεις",
|
||||||
|
"errorList": "Σφάλματα:",
|
||||||
|
"veryWeak": "Πολύ Αδύνατος",
|
||||||
|
"weak": "Αδύνατος",
|
||||||
|
"normal": "Κανονικός",
|
||||||
|
"medium": "Μέτριος",
|
||||||
|
"strong": "Δυνατός",
|
||||||
|
"veryStrong": "Πολύ Δυνατός"
|
||||||
|
}
|
21
cps/static/js/libs/pwstrength/locales/en.json
Normal file
21
cps/static/js/libs/pwstrength/locales/en.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Your password is too short",
|
||||||
|
"wordMaxLength": "Your password is too long",
|
||||||
|
"wordInvalidChar": "Your password contains an invalid character",
|
||||||
|
"wordNotEmail": "Do not use your email as your password",
|
||||||
|
"wordSimilarToUsername": "Your password cannot contain your username",
|
||||||
|
"wordTwoCharacterClasses": "Use different character classes",
|
||||||
|
"wordRepetitions": "Too many repetitions",
|
||||||
|
"wordSequences": "Your password contains sequences",
|
||||||
|
"wordLowercase": "Use at least one lowercase character",
|
||||||
|
"wordUppercase": "Use at least one uppercase character",
|
||||||
|
"wordOneNumber": "Use at least one number",
|
||||||
|
"wordOneSpecialChar": "Use at least one special character",
|
||||||
|
"errorList": "Errors:",
|
||||||
|
"veryWeak": "Very Weak",
|
||||||
|
"weak": "Weak",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Medium",
|
||||||
|
"strong": "Strong",
|
||||||
|
"veryStrong": "Very Strong"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/eo.json
Normal file
17
cps/static/js/libs/pwstrength/locales/eo.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Via pasvorto estas tro mallonga",
|
||||||
|
"wordMaxLength": "Via pasvorto estas tro longa",
|
||||||
|
"wordInvalidChar": "Via pasvorto enhavas nevalidan karaktero",
|
||||||
|
"wordNotEmail": "Ne uzu vian retpoŝtadreson kiel la pasvorton",
|
||||||
|
"wordSimilarToUsername": "Via pasvorto enhavas vian uzanto-nomon",
|
||||||
|
"wordTwoCharacterClasses": "Uzu signojn de diversaj tipoj (ekz., literoj kaj ciferoj)",
|
||||||
|
"wordRepetitions": "Tro multaj ripetiĝantaj signoj",
|
||||||
|
"wordSequences": "Via pasvorto enhavas simplan sinsekvon de signoj",
|
||||||
|
"errorList": "Eraroj:",
|
||||||
|
"veryWeak": "Trosimpla",
|
||||||
|
"weak": "Malforta",
|
||||||
|
"normal": "Mezforta",
|
||||||
|
"medium": "Akceptebla",
|
||||||
|
"strong": "Forta",
|
||||||
|
"veryStrong": "Elstare Forta"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/es.json
Normal file
17
cps/static/js/libs/pwstrength/locales/es.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Tu contraseña es demasiado corta",
|
||||||
|
"wordMaxLength": "Tu contraseña es muy larga",
|
||||||
|
"wordInvalidChar": "Tu contraseña contiene un carácter no válido",
|
||||||
|
"wordNotEmail": "No uses tu email como tu contraseña",
|
||||||
|
"wordSimilarToUsername": "Tu contraseña no puede contener tu nombre de usuario",
|
||||||
|
"wordTwoCharacterClasses": "Mezcla diferentes clases de caracteres",
|
||||||
|
"wordRepetitions": "Demasiadas repeticiones",
|
||||||
|
"wordSequences": "Tu contraseña contiene secuencias",
|
||||||
|
"errorList": "Errores:",
|
||||||
|
"veryWeak": "Muy Débil",
|
||||||
|
"weak": "Débil",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Media",
|
||||||
|
"strong": "Fuerte",
|
||||||
|
"veryStrong": "Muy Fuerte"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/fr.json
Normal file
17
cps/static/js/libs/pwstrength/locales/fr.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Votre mot de passe est trop court",
|
||||||
|
"wordMaxLength": "Votre mot de passe est trop long",
|
||||||
|
"wordInvalidChar": "Votre mot de passe contient un caractère invalide",
|
||||||
|
"wordNotEmail": "Ne pas utiliser votre adresse e-mail comme mot de passe",
|
||||||
|
"wordSimilarToUsername": "Votre mot de passe ne peut pas contenir votre nom d'utilisateur",
|
||||||
|
"wordTwoCharacterClasses": "Utilisez différents type de caractères",
|
||||||
|
"wordRepetitions": "Trop de répétitions",
|
||||||
|
"wordSequences": "Votre mot de passe contient des séquences",
|
||||||
|
"errorList": "Erreurs:",
|
||||||
|
"veryWeak": "Très Faible",
|
||||||
|
"weak": "Faible",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Moyen",
|
||||||
|
"strong": "Fort",
|
||||||
|
"veryStrong": "Très Fort"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/it.json
Normal file
17
cps/static/js/libs/pwstrength/locales/it.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "La tua password è troppo corta",
|
||||||
|
"wordMaxLength": "La tua password è troppo lunga",
|
||||||
|
"wordInvalidChar": "La tua password contiene un carattere non valido",
|
||||||
|
"wordNotEmail": "Non usare la tua e-mail come password",
|
||||||
|
"wordSimilarToUsername": "La tua password non può contenere il tuo nome",
|
||||||
|
"wordTwoCharacterClasses": "Usa classi di caratteri diversi",
|
||||||
|
"wordRepetitions": "Troppe ripetizioni",
|
||||||
|
"wordSequences": "La tua password contiene sequenze",
|
||||||
|
"errorList": "Errori:",
|
||||||
|
"veryWeak": "Molto debole",
|
||||||
|
"weak": "Debole",
|
||||||
|
"normal": "Normale",
|
||||||
|
"medium": "Media",
|
||||||
|
"strong": "Forte",
|
||||||
|
"veryStrong": "Molto forte"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/no.json
Normal file
17
cps/static/js/libs/pwstrength/locales/no.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Ditt passord er for kort",
|
||||||
|
"wordMaxLength": "Ditt passord er for langt",
|
||||||
|
"wordInvalidChar": "Ditt passord inneholder et ugyldig tegn",
|
||||||
|
"wordNotEmail": "Ikke bruk din epost som ditt passord",
|
||||||
|
"wordSimilarToUsername": "Ditt passord er for likt ditt brukernavn",
|
||||||
|
"wordTwoCharacterClasses": "Bruk en kombinasjon av bokstaver, tall og andre tegn",
|
||||||
|
"wordRepetitions": "For mange repitisjoner",
|
||||||
|
"wordSequences": "Ditt passord inneholder repeterende tegn",
|
||||||
|
"errorList": "Feil:",
|
||||||
|
"veryWeak": "Veldig Svakt",
|
||||||
|
"weak": "Svakt",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Medium",
|
||||||
|
"strong": "Sterkt",
|
||||||
|
"veryStrong": "Veldig Sterkt"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/pl.json
Normal file
17
cps/static/js/libs/pwstrength/locales/pl.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Hasło jest zbyt krótkie",
|
||||||
|
"wordMaxLength": "Hasło jest za długie",
|
||||||
|
"wordInvalidChar": "Hasło zawiera nieprawidłowy znak",
|
||||||
|
"wordNotEmail": "Hasło nie może być Twoim emailem",
|
||||||
|
"wordSimilarToUsername": "Hasło nie może zawierać nazwy użytkownika",
|
||||||
|
"wordTwoCharacterClasses": "Użyj innych klas znaków",
|
||||||
|
"wordRepetitions": "Zbyt wiele powtórzeń",
|
||||||
|
"wordSequences": "Hasło zawiera sekwencje",
|
||||||
|
"errorList": "Błędy:",
|
||||||
|
"veryWeak": "Bardzo słabe",
|
||||||
|
"weak": "Słabe",
|
||||||
|
"normal": "Normalne",
|
||||||
|
"medium": "Średnie",
|
||||||
|
"strong": "Silne",
|
||||||
|
"veryStrong": "Bardzo silne"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/pt.json
Normal file
17
cps/static/js/libs/pwstrength/locales/pt.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Sua senha é muito curta",
|
||||||
|
"wordMaxLength": "Sua senha é muito longa",
|
||||||
|
"wordInvalidChar": "Sua senha contém um caractere inválido",
|
||||||
|
"wordNotEmail": "Não use seu e-mail como senha",
|
||||||
|
"wordSimilarToUsername": "Sua senha não pode conter o seu nome de usuário",
|
||||||
|
"wordTwoCharacterClasses": "Use diferentes classes de caracteres",
|
||||||
|
"wordRepetitions": "Muitas repetições",
|
||||||
|
"wordSequences": "Sua senha contém sequências",
|
||||||
|
"errorList": "Erros:",
|
||||||
|
"veryWeak": "Muito Fraca",
|
||||||
|
"weak": "Fraca",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Média",
|
||||||
|
"strong": "Forte",
|
||||||
|
"veryStrong": "Muito Forte"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/ru.json
Normal file
17
cps/static/js/libs/pwstrength/locales/ru.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Слишком короткий пароль",
|
||||||
|
"wordMaxLength": "Ваш пароль слишком длинный",
|
||||||
|
"wordInvalidChar": "Ваш пароль содержит недопустимый символ",
|
||||||
|
"wordNotEmail": "Не используйте e-mail в качестве пароля",
|
||||||
|
"wordSimilarToUsername": "Пароль не должен содержать логин",
|
||||||
|
"wordTwoCharacterClasses": "Используйте разные классы символов",
|
||||||
|
"wordRepetitions": "Слишком много повторений",
|
||||||
|
"wordSequences": "Пароль содержит последовательности",
|
||||||
|
"errorList": "Ошибки:",
|
||||||
|
"veryWeak": "Очень слабый",
|
||||||
|
"weak": "Слабый",
|
||||||
|
"normal": "Нормальный",
|
||||||
|
"medium": "Средний",
|
||||||
|
"strong": "Сильный",
|
||||||
|
"veryStrong": "Очень сильный"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/sk.json
Normal file
17
cps/static/js/libs/pwstrength/locales/sk.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Vaše heslo je príliž krátke",
|
||||||
|
"wordMaxLength": "Vaše heslo je príliš dlhé",
|
||||||
|
"wordInvalidChar": "Vaše heslo obsahuje neplatný znak",
|
||||||
|
"wordNotEmail": "Nepoužívajte Váš email ako Vaše heslo",
|
||||||
|
"wordSimilarToUsername": "Vaše heslo nesmie obsahovať prihlasovacie meno",
|
||||||
|
"wordTwoCharacterClasses": "Použite rôzne druhy znakov",
|
||||||
|
"wordRepetitions": "Príliš veľa opakovaní",
|
||||||
|
"wordSequences": "Vaše heslo obsahuje postupnosť",
|
||||||
|
"errorList": "Chyby:",
|
||||||
|
"veryWeak": "Veľmi slabé",
|
||||||
|
"weak": "Slabé",
|
||||||
|
"normal": "Normálne",
|
||||||
|
"medium": "Stredne silné",
|
||||||
|
"strong": "Silné",
|
||||||
|
"veryStrong": "Veľmi silné"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/th.json
Normal file
17
cps/static/js/libs/pwstrength/locales/th.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "รหัสผ่านของคุณสั้นเกินไป",
|
||||||
|
"wordMaxLength": "รหัสผ่านของคุณยาวเกินไป",
|
||||||
|
"wordInvalidChar": "รหัสผ่านของคุณมีอักษรที่ไม่ถูกต้อง",
|
||||||
|
"wordNotEmail": "คุณไม่สามารถใช้รหัสผ่านเหมือนกับอีเมล์ของคุณได้",
|
||||||
|
"wordSimilarToUsername": "รหัสผ่านไม่ควรประกอบด้วยคำที่เป็น username",
|
||||||
|
"wordTwoCharacterClasses": "ลองเป็นกลุ่มคำใหม่",
|
||||||
|
"wordRepetitions": "มีอักษรซ้ำเยอะเกินไป",
|
||||||
|
"wordSequences": "รหัสผ่านของคุณเดาง่ายเกินไป",
|
||||||
|
"errorList": "Errors:",
|
||||||
|
"veryWeak": "เดาง่ายมาก",
|
||||||
|
"weak": "เดาง่าย",
|
||||||
|
"normal": "พอใช้",
|
||||||
|
"medium": "กำลังดี",
|
||||||
|
"strong": "ค่อนข้างดี",
|
||||||
|
"veryStrong": "ดีมาก"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/tr.json
Normal file
17
cps/static/js/libs/pwstrength/locales/tr.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "Girdiğiniz şifre çok Kısa",
|
||||||
|
"wordMaxLength": "Parolanız çok uzun",
|
||||||
|
"wordInvalidChar": "Şifreniz geçersiz bir karakter içeriyor",
|
||||||
|
"wordNotEmail": "E-mail adresinizi şifreniz içerisinde kullanmayınız",
|
||||||
|
"wordSimilarToUsername": "Kullanıcı Adınızı şifreniz içerisinde kullanmayınız",
|
||||||
|
"wordTwoCharacterClasses": "Başka karakter sınıfı kullanınız",
|
||||||
|
"wordRepetitions": "Çok fazla tekrar var",
|
||||||
|
"wordSequences": "Şifreniz Dizi içermektedir",
|
||||||
|
"errorList": "Hatalar:",
|
||||||
|
"veryWeak": "Çok Zayıf",
|
||||||
|
"weak": "Zayıf",
|
||||||
|
"normal": "Normal",
|
||||||
|
"medium": "Orta",
|
||||||
|
"strong": "Güçlü",
|
||||||
|
"veryStrong": "Çok Güçlü"
|
||||||
|
}
|
17
cps/static/js/libs/pwstrength/locales/zh-TW.json
Normal file
17
cps/static/js/libs/pwstrength/locales/zh-TW.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"wordMinLength": "您的密碼太短",
|
||||||
|
"wordMaxLength": "您的密碼太長",
|
||||||
|
"wordInvalidChar": "您的密碼包含無效字符",
|
||||||
|
"wordNotEmail": "不要使用電子郵件作為密碼",
|
||||||
|
"wordSimilarToUsername": "您的密碼不能包含您的用戶名",
|
||||||
|
"wordTwoCharacterClasses": "使用不同的字元類型 例如: 大小寫混合",
|
||||||
|
"wordRepetitions": "太多的重複。例如:1111",
|
||||||
|
"wordSequences": "你的密碼包含連續英/數字 例如:123 or abc",
|
||||||
|
"errorList": "錯誤:",
|
||||||
|
"veryWeak": "非常弱",
|
||||||
|
"weak": "弱",
|
||||||
|
"normal": "普通",
|
||||||
|
"medium": "中等",
|
||||||
|
"strong": "強",
|
||||||
|
"veryStrong": "非常強"
|
||||||
|
}
|
1223
cps/static/js/libs/pwstrength/pwstrength-bootstrap.js
Normal file
1223
cps/static/js/libs/pwstrength/pwstrength-bootstrap.js
Normal file
File diff suppressed because it is too large
Load Diff
4
cps/static/js/libs/pwstrength/pwstrength-bootstrap.min.js
vendored
Normal file
4
cps/static/js/libs/pwstrength/pwstrength-bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
412
cps/static/js/libs/tinymce/langs/no.js
Normal file
412
cps/static/js/libs/tinymce/langs/no.js
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
/*!
|
||||||
|
* TinyMCE Language Pack
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
|
||||||
|
* Licensed under the Tiny commercial license. See https://www.tiny.cloud/legal/
|
||||||
|
*/
|
||||||
|
tinymce.addI18n('nb_NO', {
|
||||||
|
"Redo": "Gjør om",
|
||||||
|
"Undo": "Angre",
|
||||||
|
"Cut": "Klipp ut",
|
||||||
|
"Copy": "Kopier",
|
||||||
|
"Paste": "Lim inn",
|
||||||
|
"Select all": "Marker alt",
|
||||||
|
"New document": "Nytt dokument",
|
||||||
|
"Ok": "",
|
||||||
|
"Cancel": "Avbryt",
|
||||||
|
"Visual aids": "Visuelle hjelpemidler",
|
||||||
|
"Bold": "Fet",
|
||||||
|
"Italic": "Kursiv",
|
||||||
|
"Underline": "Understreking",
|
||||||
|
"Strikethrough": "Gjennomstreking",
|
||||||
|
"Superscript": "Hevet skrift",
|
||||||
|
"Subscript": "Senket skrift",
|
||||||
|
"Clear formatting": "Fjern formateringer",
|
||||||
|
"Remove": "",
|
||||||
|
"Align left": "Venstrejuster",
|
||||||
|
"Align center": "Midtstill",
|
||||||
|
"Align right": "Høyrejuster",
|
||||||
|
"No alignment": "",
|
||||||
|
"Justify": "Blokkjuster",
|
||||||
|
"Bullet list": "Punktliste",
|
||||||
|
"Numbered list": "Nummerliste",
|
||||||
|
"Decrease indent": "Reduser innrykk",
|
||||||
|
"Increase indent": "Øk innrykk",
|
||||||
|
"Close": "Lukk",
|
||||||
|
"Formats": "Stiler",
|
||||||
|
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "Nettleseren din støtter ikke direkte tilgang til utklippsboken. Bruk istedet tastatursnarveiene Ctrl+X/C/V.",
|
||||||
|
"Headings": "Overskrifter",
|
||||||
|
"Heading 1": "Overskrift 1",
|
||||||
|
"Heading 2": "Overskrift 2",
|
||||||
|
"Heading 3": "Overskrift 3",
|
||||||
|
"Heading 4": "Overskrift 4",
|
||||||
|
"Heading 5": "Overskrift 5",
|
||||||
|
"Heading 6": "Overskrift 6",
|
||||||
|
"Preformatted": "Forhåndsformatert",
|
||||||
|
"Div": "",
|
||||||
|
"Pre": "",
|
||||||
|
"Code": "Kode",
|
||||||
|
"Paragraph": "Avsnitt",
|
||||||
|
"Blockquote": "",
|
||||||
|
"Inline": "Innkapslet",
|
||||||
|
"Blocks": "Blokker",
|
||||||
|
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Lim inn er nå i ren tekst-modus. Kopiert innhold vil bli limt inn som ren tekst inntil du slår av dette valget.",
|
||||||
|
"Fonts": "Fonter",
|
||||||
|
"Font sizes": "",
|
||||||
|
"Class": "Klasse",
|
||||||
|
"Browse for an image": "Søk etter bilde",
|
||||||
|
"OR": "",
|
||||||
|
"Drop an image here": "Slipp et bilde her",
|
||||||
|
"Upload": "Last opp",
|
||||||
|
"Uploading image": "",
|
||||||
|
"Block": "Blokk",
|
||||||
|
"Align": "Juster",
|
||||||
|
"Default": "Standard",
|
||||||
|
"Circle": "Sirkel",
|
||||||
|
"Disc": "Disk",
|
||||||
|
"Square": "Firkant",
|
||||||
|
"Lower Alpha": "Små bokstaver",
|
||||||
|
"Lower Greek": "Greske minuskler",
|
||||||
|
"Lower Roman": "Små romertall",
|
||||||
|
"Upper Alpha": "Store bokstaver",
|
||||||
|
"Upper Roman": "Store romertall",
|
||||||
|
"Anchor...": "Lenke",
|
||||||
|
"Anchor": "",
|
||||||
|
"Name": "Navn",
|
||||||
|
"ID": "",
|
||||||
|
"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "",
|
||||||
|
"You have unsaved changes are you sure you want to navigate away?": "Du har ikke arkivert endringene. Vil du fortsette uten å arkivere?",
|
||||||
|
"Restore last draft": "Gjenopprett siste utkast",
|
||||||
|
"Special character...": "Spesialtegn...",
|
||||||
|
"Special Character": "",
|
||||||
|
"Source code": "Kildekode",
|
||||||
|
"Insert/Edit code sample": "Sett inn / endre kodeeksempel",
|
||||||
|
"Language": "Språk",
|
||||||
|
"Code sample...": "Kodeeksempel",
|
||||||
|
"Left to right": "Venstre til høyre",
|
||||||
|
"Right to left": "Høyre til venstre",
|
||||||
|
"Title": "Tittel",
|
||||||
|
"Fullscreen": "Fullskjerm",
|
||||||
|
"Action": "Handling",
|
||||||
|
"Shortcut": "Snarvei",
|
||||||
|
"Help": "Hjelp",
|
||||||
|
"Address": "Adresse",
|
||||||
|
"Focus to menubar": "Fokus på menylinje",
|
||||||
|
"Focus to toolbar": "Fokus på verktøylinje",
|
||||||
|
"Focus to element path": "Fokus på elementsti",
|
||||||
|
"Focus to contextual toolbar": "Fokus på kontekstuell verktøylinje",
|
||||||
|
"Insert link (if link plugin activated)": "Sett inn lenke (dersom lenketillegg er aktivert)",
|
||||||
|
"Save (if save plugin activated)": "Lagre (dersom lagretillegg er aktivert)",
|
||||||
|
"Find (if searchreplace plugin activated)": "Finn (dersom tillegg for søk og erstatt er aktivert)",
|
||||||
|
"Plugins installed ({0}):": "Installerte tillegg ({0}):",
|
||||||
|
"Premium plugins:": "Premiumtillegg:",
|
||||||
|
"Learn more...": "Les mer ...",
|
||||||
|
"You are using {0}": "Du bruker {0}",
|
||||||
|
"Plugins": "Programtillegg",
|
||||||
|
"Handy Shortcuts": "Nyttige snarveier",
|
||||||
|
"Horizontal line": "Horisontal linje",
|
||||||
|
"Insert/edit image": "Sett inn / rediger bilde",
|
||||||
|
"Alternative description": "Alternativ beskrivelse",
|
||||||
|
"Accessibility": "Tilgjengelighet",
|
||||||
|
"Image is decorative": "Bilde er dekorasjon",
|
||||||
|
"Source": "Kilde",
|
||||||
|
"Dimensions": "Størrelser",
|
||||||
|
"Constrain proportions": "Begrens proporsjoner",
|
||||||
|
"General": "Generelt",
|
||||||
|
"Advanced": "Avansert",
|
||||||
|
"Style": "Stil",
|
||||||
|
"Vertical space": "Vertikal avstand",
|
||||||
|
"Horizontal space": "Horisontal avstand",
|
||||||
|
"Border": "Ramme",
|
||||||
|
"Insert image": "Sett inn bilde",
|
||||||
|
"Image...": "Bilde...",
|
||||||
|
"Image list": "Bildeliste",
|
||||||
|
"Resize": "Skaler",
|
||||||
|
"Insert date/time": "Sett inn dato/tid",
|
||||||
|
"Date/time": "Dato/tid",
|
||||||
|
"Insert/edit link": "Sett inn / rediger lenke",
|
||||||
|
"Text to display": "Tekst som skal vises",
|
||||||
|
"Url": "",
|
||||||
|
"Open link in...": "Åpne lenke i..",
|
||||||
|
"Current window": "Nåværende vindu",
|
||||||
|
"None": "Ingen",
|
||||||
|
"New window": "Nytt vindu",
|
||||||
|
"Open link": "Åpne lenke",
|
||||||
|
"Remove link": "Fjern lenke",
|
||||||
|
"Anchors": "Forankringspunkter",
|
||||||
|
"Link...": "Lenke...",
|
||||||
|
"Paste or type a link": "Lim inn eller skriv en lenke",
|
||||||
|
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "Oppgitt URL ser ut til å være en e-postadresse. Ønsker du å sette inn påkrevet mailto: prefiks foran e-postadressen?",
|
||||||
|
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "URL du skrev inn ser ut som en ekstern adresse. Vil du legge til det obligatoriske prefikset http://?",
|
||||||
|
"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "Nettadressen du fylte inn ser ut til å være en ekstern. Ønsker du å legge til påkrevd 'https://'-prefiks?",
|
||||||
|
"Link list": "Liste over lenker",
|
||||||
|
"Insert video": "Sett inn video",
|
||||||
|
"Insert/edit video": "Sett inn / rediger video",
|
||||||
|
"Insert/edit media": "Sett inn / endre media",
|
||||||
|
"Alternative source": "Alternativ kilde",
|
||||||
|
"Alternative source URL": "Alternativ kilde URL",
|
||||||
|
"Media poster (Image URL)": "Mediaposter (bilde-URL)",
|
||||||
|
"Paste your embed code below:": "Lim inn inkluderingskoden nedenfor:",
|
||||||
|
"Embed": "Inkluder",
|
||||||
|
"Media...": "Media..",
|
||||||
|
"Nonbreaking space": "Hardt mellomrom",
|
||||||
|
"Page break": "Sideskifte",
|
||||||
|
"Paste as text": "Lim inn som tekst",
|
||||||
|
"Preview": "Forhåndsvis",
|
||||||
|
"Print": "",
|
||||||
|
"Print...": "Skriv ut...",
|
||||||
|
"Save": "Lagre",
|
||||||
|
"Find": "Søk etter",
|
||||||
|
"Replace with": "Erstatt med",
|
||||||
|
"Replace": "Erstatt",
|
||||||
|
"Replace all": "Erstatt alle",
|
||||||
|
"Previous": "Forrige",
|
||||||
|
"Next": "Neste",
|
||||||
|
"Find and Replace": "Finn og erstatt",
|
||||||
|
"Find and replace...": "Finn og erstatt...",
|
||||||
|
"Could not find the specified string.": "Kunne ikke finne den spesifiserte teksten",
|
||||||
|
"Match case": "Skill mellom store / små bokstaver",
|
||||||
|
"Find whole words only": "Finn kun hele ord",
|
||||||
|
"Find in selection": "Finn i utvalg",
|
||||||
|
"Insert table": "Sett inn tabell",
|
||||||
|
"Table properties": "Tabellegenskaper",
|
||||||
|
"Delete table": "Slett tabell",
|
||||||
|
"Cell": "Celle",
|
||||||
|
"Row": "Rad",
|
||||||
|
"Column": "Kolonne",
|
||||||
|
"Cell properties": "Celleegenskaper",
|
||||||
|
"Merge cells": "Slå sammen celler",
|
||||||
|
"Split cell": "Splitt celle",
|
||||||
|
"Insert row before": "Sett inn rad før",
|
||||||
|
"Insert row after": "Sett inn rad etter",
|
||||||
|
"Delete row": "Slett rad",
|
||||||
|
"Row properties": "Radegenskaper",
|
||||||
|
"Cut row": "Klipp ut rad",
|
||||||
|
"Cut column": "",
|
||||||
|
"Copy row": "Kopier rad",
|
||||||
|
"Copy column": "",
|
||||||
|
"Paste row before": "Lim inn rad før",
|
||||||
|
"Paste column before": "",
|
||||||
|
"Paste row after": "Lim inn rad etter",
|
||||||
|
"Paste column after": "",
|
||||||
|
"Insert column before": "Sett inn kolonne før",
|
||||||
|
"Insert column after": "Sett inn kolonne etter",
|
||||||
|
"Delete column": "Slett kolonne",
|
||||||
|
"Cols": "Kolonner",
|
||||||
|
"Rows": "Rader",
|
||||||
|
"Width": "Bredde",
|
||||||
|
"Height": "Høyde",
|
||||||
|
"Cell spacing": "Celleavstand",
|
||||||
|
"Cell padding": "Cellemarg",
|
||||||
|
"Row clipboard actions": "",
|
||||||
|
"Column clipboard actions": "",
|
||||||
|
"Table styles": "",
|
||||||
|
"Cell styles": "",
|
||||||
|
"Column header": "",
|
||||||
|
"Row header": "",
|
||||||
|
"Table caption": "",
|
||||||
|
"Caption": "Bildetekst",
|
||||||
|
"Show caption": "Vis bildetekst",
|
||||||
|
"Left": "Venstre",
|
||||||
|
"Center": "Senter",
|
||||||
|
"Right": "Høyre",
|
||||||
|
"Cell type": "Celletype",
|
||||||
|
"Scope": "Omfang",
|
||||||
|
"Alignment": "Justering",
|
||||||
|
"Horizontal align": "",
|
||||||
|
"Vertical align": "",
|
||||||
|
"Top": "Topp",
|
||||||
|
"Middle": "Sentrert",
|
||||||
|
"Bottom": "Bunn",
|
||||||
|
"Header cell": "Overskriftscelle",
|
||||||
|
"Row group": "Radgruppe",
|
||||||
|
"Column group": "Kolonnegruppe",
|
||||||
|
"Row type": "Radtype",
|
||||||
|
"Header": "",
|
||||||
|
"Body": "Brødtekst",
|
||||||
|
"Footer": "Bunntekst",
|
||||||
|
"Border color": "Rammefarge",
|
||||||
|
"Solid": "",
|
||||||
|
"Dotted": "",
|
||||||
|
"Dashed": "",
|
||||||
|
"Double": "",
|
||||||
|
"Groove": "",
|
||||||
|
"Ridge": "",
|
||||||
|
"Inset": "",
|
||||||
|
"Outset": "",
|
||||||
|
"Hidden": "",
|
||||||
|
"Insert template...": "Sett inn mal..",
|
||||||
|
"Templates": "Maler",
|
||||||
|
"Template": "Mal",
|
||||||
|
"Insert Template": "",
|
||||||
|
"Text color": "Tekstfarge",
|
||||||
|
"Background color": "Bakgrunnsfarge",
|
||||||
|
"Custom...": "Tilpasset...",
|
||||||
|
"Custom color": "Tilpasset farge",
|
||||||
|
"No color": "Ingen farge",
|
||||||
|
"Remove color": "Fjern farge",
|
||||||
|
"Show blocks": "Vis blokker",
|
||||||
|
"Show invisible characters": "Vis skjulte tegn",
|
||||||
|
"Word count": "Ordtelling",
|
||||||
|
"Count": "Opptelling",
|
||||||
|
"Document": "Dokument",
|
||||||
|
"Selection": "Utvalg",
|
||||||
|
"Words": "Ord",
|
||||||
|
"Words: {0}": "Ord: {0}",
|
||||||
|
"{0} words": "{0} ord",
|
||||||
|
"File": "Fil",
|
||||||
|
"Edit": "Rediger",
|
||||||
|
"Insert": "Sett inn",
|
||||||
|
"View": "Vis",
|
||||||
|
"Format": "",
|
||||||
|
"Table": "Tabell",
|
||||||
|
"Tools": "Verktøy",
|
||||||
|
"Powered by {0}": "Drevet av {0}",
|
||||||
|
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Tekstredigering. Tast ALT-F9 for meny. Tast ALT-F10 for verktøylinje. Tast ALT-0 for hjelp.",
|
||||||
|
"Image title": "Bildetittel",
|
||||||
|
"Border width": "Bordbredde",
|
||||||
|
"Border style": "Bordstil",
|
||||||
|
"Error": "Feil",
|
||||||
|
"Warn": "Advarsel",
|
||||||
|
"Valid": "Gyldig",
|
||||||
|
"To open the popup, press Shift+Enter": "For å åpne popup, trykk Shift+Enter",
|
||||||
|
"Rich Text Area": "",
|
||||||
|
"Rich Text Area. Press ALT-0 for help.": "Rik-tekstområde. Trykk ALT-0 for hjelp.",
|
||||||
|
"System Font": "Systemfont",
|
||||||
|
"Failed to upload image: {0}": "Opplasting av bilde feilet: {0}",
|
||||||
|
"Failed to load plugin: {0} from url {1}": "Kunne ikke laste tillegg: {0} from url {1}",
|
||||||
|
"Failed to load plugin url: {0}": "Kunne ikke laste tillegg url: {0}",
|
||||||
|
"Failed to initialize plugin: {0}": "Kunne ikke initialisere tillegg: {0}",
|
||||||
|
"example": "eksempel",
|
||||||
|
"Search": "Søk",
|
||||||
|
"All": "Alle",
|
||||||
|
"Currency": "Valuta",
|
||||||
|
"Text": "Tekst",
|
||||||
|
"Quotations": "Sitater",
|
||||||
|
"Mathematical": "Matematisk",
|
||||||
|
"Extended Latin": "Utvidet latin",
|
||||||
|
"Symbols": "Symboler",
|
||||||
|
"Arrows": "Piler",
|
||||||
|
"User Defined": "Brukerdefinert",
|
||||||
|
"dollar sign": "dollartegn",
|
||||||
|
"currency sign": "valutasymbol",
|
||||||
|
"euro-currency sign": "Euro-valutasymbol",
|
||||||
|
"colon sign": "kolon-symbol",
|
||||||
|
"cruzeiro sign": "cruzeiro-symbol",
|
||||||
|
"french franc sign": "franske franc-symbol",
|
||||||
|
"lira sign": "lire-symbol",
|
||||||
|
"mill sign": "mill-symbol",
|
||||||
|
"naira sign": "naira-symbol",
|
||||||
|
"peseta sign": "peseta-symbol",
|
||||||
|
"rupee sign": "rupee-symbol",
|
||||||
|
"won sign": "won-symbol",
|
||||||
|
"new sheqel sign": "Ny sheqel-symbol",
|
||||||
|
"dong sign": "dong-symbol",
|
||||||
|
"kip sign": "kip-symbol",
|
||||||
|
"tugrik sign": "tugrik-symbol",
|
||||||
|
"drachma sign": "drachma-symbol",
|
||||||
|
"german penny symbol": "tysk penny-symbol",
|
||||||
|
"peso sign": "peso-symbol",
|
||||||
|
"guarani sign": "quarani-symbol",
|
||||||
|
"austral sign": "austral-symbol",
|
||||||
|
"hryvnia sign": "hryvina-symbol",
|
||||||
|
"cedi sign": "credi-symbol",
|
||||||
|
"livre tournois sign": "livre tournois-symbol",
|
||||||
|
"spesmilo sign": "spesmilo-symbol",
|
||||||
|
"tenge sign": "tenge-symbol",
|
||||||
|
"indian rupee sign": "indisk rupee-symbol",
|
||||||
|
"turkish lira sign": "tyrkisk lire-symbol",
|
||||||
|
"nordic mark sign": "nordisk mark-symbol",
|
||||||
|
"manat sign": "manat-symbol",
|
||||||
|
"ruble sign": "ruble-symbol",
|
||||||
|
"yen character": "yen-symbol",
|
||||||
|
"yuan character": "yuan-symbol",
|
||||||
|
"yuan character, in hong kong and taiwan": "yuan-symbol, i Hongkong og Taiwan",
|
||||||
|
"yen/yuan character variant one": "yen/yuan-symbol variant en",
|
||||||
|
"Emojis": "",
|
||||||
|
"Emojis...": "",
|
||||||
|
"Loading emojis...": "",
|
||||||
|
"Could not load emojis": "",
|
||||||
|
"People": "Mennesker",
|
||||||
|
"Animals and Nature": "Dyr og natur",
|
||||||
|
"Food and Drink": "Mat og drikke",
|
||||||
|
"Activity": "Aktivitet",
|
||||||
|
"Travel and Places": "Reise og steder",
|
||||||
|
"Objects": "Objekter",
|
||||||
|
"Flags": "Flagg",
|
||||||
|
"Characters": "Tegn",
|
||||||
|
"Characters (no spaces)": "Tegn (uten mellomrom)",
|
||||||
|
"{0} characters": "{0} tegn",
|
||||||
|
"Error: Form submit field collision.": "Feil: Skjemafelt innsendingskollisjon.",
|
||||||
|
"Error: No form element found.": "Feil: Intet skjemafelt funnet.",
|
||||||
|
"Color swatch": "Fargepalett",
|
||||||
|
"Color Picker": "Fargevelger",
|
||||||
|
"Invalid hex color code: {0}": "",
|
||||||
|
"Invalid input": "",
|
||||||
|
"R": "",
|
||||||
|
"Red component": "",
|
||||||
|
"G": "",
|
||||||
|
"Green component": "",
|
||||||
|
"B": "",
|
||||||
|
"Blue component": "",
|
||||||
|
"#": "",
|
||||||
|
"Hex color code": "",
|
||||||
|
"Range 0 to 255": "",
|
||||||
|
"Turquoise": "Turkis",
|
||||||
|
"Green": "Grønn",
|
||||||
|
"Blue": "Blå",
|
||||||
|
"Purple": "Lilla",
|
||||||
|
"Navy Blue": "Marineblå",
|
||||||
|
"Dark Turquoise": "Mørk turkis",
|
||||||
|
"Dark Green": "Mørkegrønn",
|
||||||
|
"Medium Blue": "Mellomblå",
|
||||||
|
"Medium Purple": "Medium lilla",
|
||||||
|
"Midnight Blue": "Midnattblå",
|
||||||
|
"Yellow": "Gul",
|
||||||
|
"Orange": "Oransje",
|
||||||
|
"Red": "Rød",
|
||||||
|
"Light Gray": "Lys grå",
|
||||||
|
"Gray": "Grå",
|
||||||
|
"Dark Yellow": "Mørk gul",
|
||||||
|
"Dark Orange": "Mørk oransje",
|
||||||
|
"Dark Red": "Mørkerød",
|
||||||
|
"Medium Gray": "Medium grå",
|
||||||
|
"Dark Gray": "Mørk grå",
|
||||||
|
"Light Green": "Lys grønn",
|
||||||
|
"Light Yellow": "Lys gul",
|
||||||
|
"Light Red": "Lys rød",
|
||||||
|
"Light Purple": "Lys lilla",
|
||||||
|
"Light Blue": "Lys blå",
|
||||||
|
"Dark Purple": "Mørk lilla",
|
||||||
|
"Dark Blue": "Mørk blå",
|
||||||
|
"Black": "Svart",
|
||||||
|
"White": "Hvit",
|
||||||
|
"Switch to or from fullscreen mode": "Bytt til eller fra fullskjermmodus",
|
||||||
|
"Open help dialog": "Åpne hjelp-dialog",
|
||||||
|
"history": "historikk",
|
||||||
|
"styles": "stiler",
|
||||||
|
"formatting": "formatering",
|
||||||
|
"alignment": "justering",
|
||||||
|
"indentation": "innrykk",
|
||||||
|
"Font": "Skrift",
|
||||||
|
"Size": "Størrelse",
|
||||||
|
"More...": "Mer...",
|
||||||
|
"Select...": "Velg...",
|
||||||
|
"Preferences": "Innstillinger",
|
||||||
|
"Yes": "Ja",
|
||||||
|
"No": "Nei",
|
||||||
|
"Keyboard Navigation": "Navigering med tastaturet",
|
||||||
|
"Version": "Versjon",
|
||||||
|
"Code view": "Kodevisning",
|
||||||
|
"Open popup menu for split buttons": "Åpne sprettoppmeny for splitt-knapper",
|
||||||
|
"List Properties": "Listeegenskaper",
|
||||||
|
"List properties...": "Listeegenskaper ...",
|
||||||
|
"Start list at number": "Start liste på nummer",
|
||||||
|
"Line height": "Linjehøyde",
|
||||||
|
"Dropped file type is not supported": "",
|
||||||
|
"Loading...": "",
|
||||||
|
"ImageProxy HTTP error: Rejected request": "",
|
||||||
|
"ImageProxy HTTP error: Could not find Image Proxy": "",
|
||||||
|
"ImageProxy HTTP error: Incorrect Image Proxy URL": "",
|
||||||
|
"ImageProxy HTTP error: Unknown ImageProxy error": ""
|
||||||
|
});
|
462
cps/static/js/libs/tinymce/langs/pt.js
Normal file
462
cps/static/js/libs/tinymce/langs/pt.js
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
tinymce.addI18n('pt_PT',{
|
||||||
|
"Redo": "Refazer",
|
||||||
|
"Undo": "Anular",
|
||||||
|
"Cut": "Cortar",
|
||||||
|
"Copy": "Copiar",
|
||||||
|
"Paste": "Colar",
|
||||||
|
"Select all": "Selecionar tudo",
|
||||||
|
"New document": "Novo documento",
|
||||||
|
"Ok": "Ok",
|
||||||
|
"Cancel": "Cancelar",
|
||||||
|
"Visual aids": "Ajuda visual",
|
||||||
|
"Bold": "Negrito",
|
||||||
|
"Italic": "It\u00e1lico",
|
||||||
|
"Underline": "Sublinhado",
|
||||||
|
"Strikethrough": "Rasurado",
|
||||||
|
"Superscript": "Superior \u00e0 linha",
|
||||||
|
"Subscript": "Inferior \u00e0 linha",
|
||||||
|
"Clear formatting": "Limpar formata\u00e7\u00e3o",
|
||||||
|
"Align left": "Alinhar \u00e0 esquerda",
|
||||||
|
"Align center": "Alinhar ao centro",
|
||||||
|
"Align right": "Alinhar \u00e0 direita",
|
||||||
|
"Justify": "Justificar",
|
||||||
|
"Bullet list": "Lista com marcas",
|
||||||
|
"Numbered list": "Lista numerada",
|
||||||
|
"Decrease indent": "Diminuir avan\u00e7o",
|
||||||
|
"Increase indent": "Aumentar avan\u00e7o",
|
||||||
|
"Close": "Fechar",
|
||||||
|
"Formats": "Formatos",
|
||||||
|
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "O seu navegador n\u00e3o suporta acesso direto \u00e0 \u00e1rea de transfer\u00eancia. Por favor, use os atalhos Ctrl+X\/C\/V do seu teclado.",
|
||||||
|
"Headers": "Cabe\u00e7alhos",
|
||||||
|
"Header 1": "Cabe\u00e7alho 1",
|
||||||
|
"Header 2": "Cabe\u00e7alho 2",
|
||||||
|
"Header 3": "Cabe\u00e7alho 3",
|
||||||
|
"Header 4": "Cabe\u00e7alho 4",
|
||||||
|
"Header 5": "Cabe\u00e7alho 5",
|
||||||
|
"Header 6": "Cabe\u00e7alho 6",
|
||||||
|
"Headings": "T\u00edtulos",
|
||||||
|
"Heading 1": "T\u00edtulo 1",
|
||||||
|
"Heading 2": "T\u00edtulo 2",
|
||||||
|
"Heading 3": "T\u00edtulo 3",
|
||||||
|
"Heading 4": "T\u00edtulo 4",
|
||||||
|
"Heading 5": "T\u00edtulo 5",
|
||||||
|
"Heading 6": "T\u00edtulo 6",
|
||||||
|
"Preformatted": "Pr\u00e9-formatado",
|
||||||
|
"Div": "Div",
|
||||||
|
"Pre": "Pre",
|
||||||
|
"Code": "C\u00f3digo",
|
||||||
|
"Paragraph": "Par\u00e1grafo",
|
||||||
|
"Blockquote": "Blockquote",
|
||||||
|
"Inline": "Inline",
|
||||||
|
"Blocks": "Blocos",
|
||||||
|
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "O comando colar est\u00e1 em modo de texto simples. O conte\u00fado ser\u00e1 colado como texto simples at\u00e9 desativar esta op\u00e7\u00e3o.",
|
||||||
|
"Fonts": "Tipos de letra",
|
||||||
|
"Font Sizes": "Tamanhos dos tipos de letra",
|
||||||
|
"Class": "Classe",
|
||||||
|
"Browse for an image": "Procurar uma imagem",
|
||||||
|
"OR": "OU",
|
||||||
|
"Drop an image here": "Largar aqui uma imagem",
|
||||||
|
"Upload": "Carregar",
|
||||||
|
"Block": "Bloco",
|
||||||
|
"Align": "Alinhar",
|
||||||
|
"Default": "Padr\u00e3o",
|
||||||
|
"Circle": "C\u00edrculo",
|
||||||
|
"Disc": "Disco",
|
||||||
|
"Square": "Quadrado",
|
||||||
|
"Lower Alpha": "a. b. c. ...",
|
||||||
|
"Lower Greek": "\\u03b1. \\u03b2. \\u03b3. ...",
|
||||||
|
"Lower Roman": "i. ii. iii. ...",
|
||||||
|
"Upper Alpha": "A. B. C. ...",
|
||||||
|
"Upper Roman": "I. II. III. ...",
|
||||||
|
"Anchor...": "\u00c2ncora...",
|
||||||
|
"Name": "Nome",
|
||||||
|
"Id": "ID",
|
||||||
|
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "O ID deve come\u00e7ar com uma letra, seguido apenas por letras, n\u00fameros, pontos, dois pontos, tra\u00e7os ou sobtra\u00e7os.",
|
||||||
|
"You have unsaved changes are you sure you want to navigate away?": "Existem altera\u00e7\u00f5es que ainda n\u00e3o foram guardadas. Tem a certeza que pretende sair?",
|
||||||
|
"Restore last draft": "Restaurar o \u00faltimo rascunho",
|
||||||
|
"Special character...": "Car\u00e1ter especial...",
|
||||||
|
"Source code": "C\u00f3digo fonte",
|
||||||
|
"Insert\/Edit code sample": "Inserir\/editar amostra de c\u00f3digo",
|
||||||
|
"Language": "Idioma",
|
||||||
|
"Code sample...": "Amostra de c\u00f3digo...",
|
||||||
|
"Color Picker": "Seletor de cores",
|
||||||
|
"R": "R",
|
||||||
|
"G": "G",
|
||||||
|
"B": "B",
|
||||||
|
"Left to right": "Da esquerda para a direita",
|
||||||
|
"Right to left": "Da direita para a esquerda",
|
||||||
|
"Emoticons": "Emo\u00e7\u00f5es",
|
||||||
|
"Emoticons...": "\u00cdcones expressivos...",
|
||||||
|
"Metadata and Document Properties": "Metadados e propriedades do documento",
|
||||||
|
"Title": "T\u00edtulo",
|
||||||
|
"Keywords": "Palavras-chave",
|
||||||
|
"Description": "Descri\u00e7\u00e3o",
|
||||||
|
"Robots": "Rob\u00f4s",
|
||||||
|
"Author": "Autor",
|
||||||
|
"Encoding": "Codifica\u00e7\u00e3o",
|
||||||
|
"Fullscreen": "Ecr\u00e3 completo",
|
||||||
|
"Action": "A\u00e7\u00e3o",
|
||||||
|
"Shortcut": "Atalho",
|
||||||
|
"Help": "Ajuda",
|
||||||
|
"Address": "Endere\u00e7o",
|
||||||
|
"Focus to menubar": "Foco na barra de menu",
|
||||||
|
"Focus to toolbar": "Foco na barra de ferramentas",
|
||||||
|
"Focus to element path": "Foco no caminho do elemento",
|
||||||
|
"Focus to contextual toolbar": "Foco na barra de contexto",
|
||||||
|
"Insert link (if link plugin activated)": "Inserir hiperliga\u00e7\u00e3o (se o plugin de liga\u00e7\u00f5es estiver ativado)",
|
||||||
|
"Save (if save plugin activated)": "Guardar (se o plugin de guardar estiver ativado)",
|
||||||
|
"Find (if searchreplace plugin activated)": "Pesquisar (se o plugin pesquisar e substituir estiver ativado)",
|
||||||
|
"Plugins installed ({0}):": "Plugins instalados ({0}):",
|
||||||
|
"Premium plugins:": "Plugins comerciais:",
|
||||||
|
"Learn more...": "Saiba mais...",
|
||||||
|
"You are using {0}": "Est\u00e1 a usar {0}",
|
||||||
|
"Plugins": "Plugins",
|
||||||
|
"Handy Shortcuts": "Atalhos \u00fateis",
|
||||||
|
"Horizontal line": "Linha horizontal",
|
||||||
|
"Insert\/edit image": "Inserir\/editar imagem",
|
||||||
|
"Alternative description": "Descri\u00e7\u00e3o alternativa",
|
||||||
|
"Accessibility": "Acessibilidade",
|
||||||
|
"Image is decorative": "Imagem \u00e9 decorativa",
|
||||||
|
"Source": "Localiza\u00e7\u00e3o",
|
||||||
|
"Dimensions": "Dimens\u00f5es",
|
||||||
|
"Constrain proportions": "Manter propor\u00e7\u00f5es",
|
||||||
|
"General": "Geral",
|
||||||
|
"Advanced": "Avan\u00e7ado",
|
||||||
|
"Style": "Estilo",
|
||||||
|
"Vertical space": "Espa\u00e7amento vertical",
|
||||||
|
"Horizontal space": "Espa\u00e7amento horizontal",
|
||||||
|
"Border": "Contorno",
|
||||||
|
"Insert image": "Inserir imagem",
|
||||||
|
"Image...": "Imagem...",
|
||||||
|
"Image list": "Lista de imagens",
|
||||||
|
"Rotate counterclockwise": "Rota\u00e7\u00e3o anti-hor\u00e1ria",
|
||||||
|
"Rotate clockwise": "Rota\u00e7\u00e3o hor\u00e1ria",
|
||||||
|
"Flip vertically": "Inverter verticalmente",
|
||||||
|
"Flip horizontally": "Inverter horizontalmente",
|
||||||
|
"Edit image": "Editar imagem",
|
||||||
|
"Image options": "Op\u00e7\u00f5es de imagem",
|
||||||
|
"Zoom in": "Mais zoom",
|
||||||
|
"Zoom out": "Menos zoom",
|
||||||
|
"Crop": "Recortar",
|
||||||
|
"Resize": "Redimensionar",
|
||||||
|
"Orientation": "Orienta\u00e7\u00e3o",
|
||||||
|
"Brightness": "Brilho",
|
||||||
|
"Sharpen": "Mais nitidez",
|
||||||
|
"Contrast": "Contraste",
|
||||||
|
"Color levels": "N\u00edveis de cor",
|
||||||
|
"Gamma": "Gama",
|
||||||
|
"Invert": "Inverter",
|
||||||
|
"Apply": "Aplicar",
|
||||||
|
"Back": "Voltar",
|
||||||
|
"Insert date\/time": "Inserir data\/hora",
|
||||||
|
"Date\/time": "Data\/hora",
|
||||||
|
"Insert\/edit link": "Inserir\/editar liga\u00e7\u00e3o",
|
||||||
|
"Text to display": "Texto a exibir",
|
||||||
|
"Url": "URL",
|
||||||
|
"Open link in...": "Abrir liga\u00e7\u00e3o em...",
|
||||||
|
"Current window": "Janela atual",
|
||||||
|
"None": "Nenhum",
|
||||||
|
"New window": "Nova janela",
|
||||||
|
"Open link": "Abrir liga\u00e7\u00e3o",
|
||||||
|
"Remove link": "Remover liga\u00e7\u00e3o",
|
||||||
|
"Anchors": "\u00c2ncora",
|
||||||
|
"Link...": "Liga\u00e7\u00e3o...",
|
||||||
|
"Paste or type a link": "Copiar ou escrever uma hiperliga\u00e7\u00e3o",
|
||||||
|
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "O URL que indicou parece ser um endere\u00e7o de email. Quer adicionar o prefixo mailto: tal como necess\u00e1rio?",
|
||||||
|
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "O URL que indicou parece ser um endere\u00e7o web. Quer adicionar o prefixo http:\/\/ tal como necess\u00e1rio?",
|
||||||
|
"The URL you entered seems to be an external link. Do you want to add the required https:\/\/ prefix?": "O URL que introduziu parece ser uma liga\u00e7\u00e3o externa. Deseja adicionar-lhe o prefixo https:\/\/ ?",
|
||||||
|
"Link list": "Lista de liga\u00e7\u00f5es",
|
||||||
|
"Insert video": "Inserir v\u00eddeo",
|
||||||
|
"Insert\/edit video": "Inserir\/editar v\u00eddeo",
|
||||||
|
"Insert\/edit media": "Inserir\/editar media",
|
||||||
|
"Alternative source": "Localiza\u00e7\u00e3o alternativa",
|
||||||
|
"Alternative source URL": "URL da origem alternativa",
|
||||||
|
"Media poster (Image URL)": "Publicador de media (URL da imagem)",
|
||||||
|
"Paste your embed code below:": "Colar c\u00f3digo para embeber:",
|
||||||
|
"Embed": "Embeber",
|
||||||
|
"Media...": "Media...",
|
||||||
|
"Nonbreaking space": "Espa\u00e7o n\u00e3o quebr\u00e1vel",
|
||||||
|
"Page break": "Quebra de p\u00e1gina",
|
||||||
|
"Paste as text": "Colar como texto",
|
||||||
|
"Preview": "Pr\u00e9-visualizar",
|
||||||
|
"Print...": "Imprimir...",
|
||||||
|
"Save": "Guardar",
|
||||||
|
"Find": "Pesquisar",
|
||||||
|
"Replace with": "Substituir por",
|
||||||
|
"Replace": "Substituir",
|
||||||
|
"Replace all": "Substituir tudo",
|
||||||
|
"Previous": "Anterior",
|
||||||
|
"Next": "Pr\u00f3ximo",
|
||||||
|
"Find and Replace": "Pesquisar e substituir",
|
||||||
|
"Find and replace...": "Localizar e substituir...",
|
||||||
|
"Could not find the specified string.": "N\u00e3o foi poss\u00edvel localizar o termo especificado.",
|
||||||
|
"Match case": "Diferenciar mai\u00fasculas e min\u00fasculas",
|
||||||
|
"Find whole words only": "Localizar apenas palavras inteiras",
|
||||||
|
"Find in selection": "Pesquisar na selec\u00e7\u00e3o",
|
||||||
|
"Spellcheck": "Corretor ortogr\u00e1fico",
|
||||||
|
"Spellcheck Language": "Idioma de verifica\u00e7\u00e3o lingu\u00edstica",
|
||||||
|
"No misspellings found.": "N\u00e3o foram encontrados erros ortogr\u00e1ficos.",
|
||||||
|
"Ignore": "Ignorar",
|
||||||
|
"Ignore all": "Ignorar tudo",
|
||||||
|
"Finish": "Concluir",
|
||||||
|
"Add to Dictionary": "Adicionar ao dicion\u00e1rio",
|
||||||
|
"Insert table": "Inserir tabela",
|
||||||
|
"Table properties": "Propriedades da tabela",
|
||||||
|
"Delete table": "Eliminar tabela",
|
||||||
|
"Cell": "C\u00e9lula",
|
||||||
|
"Row": "Linha",
|
||||||
|
"Column": "Coluna",
|
||||||
|
"Cell properties": "Propriedades da c\u00e9lula",
|
||||||
|
"Merge cells": "Unir c\u00e9lulas",
|
||||||
|
"Split cell": "Dividir c\u00e9lula",
|
||||||
|
"Insert row before": "Inserir linha antes",
|
||||||
|
"Insert row after": "Inserir linha depois",
|
||||||
|
"Delete row": "Eliminar linha",
|
||||||
|
"Row properties": "Propriedades da linha",
|
||||||
|
"Cut row": "Cortar linha",
|
||||||
|
"Copy row": "Copiar linha",
|
||||||
|
"Paste row before": "Colar linha antes",
|
||||||
|
"Paste row after": "Colar linha depois",
|
||||||
|
"Insert column before": "Inserir coluna antes",
|
||||||
|
"Insert column after": "Inserir coluna depois",
|
||||||
|
"Delete column": "Eliminar coluna",
|
||||||
|
"Cols": "Colunas",
|
||||||
|
"Rows": "Linhas",
|
||||||
|
"Width": "Largura",
|
||||||
|
"Height": "Altura",
|
||||||
|
"Cell spacing": "Espa\u00e7amento entre c\u00e9lulas",
|
||||||
|
"Cell padding": "Espa\u00e7amento interno da c\u00e9lula",
|
||||||
|
"Caption": "Legenda",
|
||||||
|
"Show caption": "Mostrar legenda",
|
||||||
|
"Left": "Esquerda",
|
||||||
|
"Center": "Centro",
|
||||||
|
"Right": "Direita",
|
||||||
|
"Cell type": "Tipo de c\u00e9lula",
|
||||||
|
"Scope": "Escopo",
|
||||||
|
"Alignment": "Alinhamento",
|
||||||
|
"H Align": "Alinhamento H",
|
||||||
|
"V Align": "Alinhamento V",
|
||||||
|
"Top": "Superior",
|
||||||
|
"Middle": "Meio",
|
||||||
|
"Bottom": "Inferior",
|
||||||
|
"Header cell": "C\u00e9lula de cabe\u00e7alho",
|
||||||
|
"Row group": "Agrupar linha",
|
||||||
|
"Column group": "Agrupar coluna",
|
||||||
|
"Row type": "Tipo de linha",
|
||||||
|
"Header": "Cabe\u00e7alho",
|
||||||
|
"Body": "Corpo",
|
||||||
|
"Footer": "Rodap\u00e9",
|
||||||
|
"Border color": "Cor de contorno",
|
||||||
|
"Insert template...": "Inserir modelo...",
|
||||||
|
"Templates": "Modelos",
|
||||||
|
"Template": "Tema",
|
||||||
|
"Text color": "Cor do texto",
|
||||||
|
"Background color": "Cor de fundo",
|
||||||
|
"Custom...": "Personalizada...",
|
||||||
|
"Custom color": "Cor personalizada",
|
||||||
|
"No color": "Sem cor",
|
||||||
|
"Remove color": "Remover cor",
|
||||||
|
"Table of Contents": "\u00cdndice",
|
||||||
|
"Show blocks": "Mostrar blocos",
|
||||||
|
"Show invisible characters": "Mostrar caracteres invis\u00edveis",
|
||||||
|
"Word count": "Contagem de palavras",
|
||||||
|
"Count": "Contagem",
|
||||||
|
"Document": "Documento",
|
||||||
|
"Selection": "Sele\u00e7\u00e3o",
|
||||||
|
"Words": "Palavras",
|
||||||
|
"Words: {0}": "Palavras: {0}",
|
||||||
|
"{0} words": "{0} palavras",
|
||||||
|
"File": "Ficheiro",
|
||||||
|
"Edit": "Editar",
|
||||||
|
"Insert": "Inserir",
|
||||||
|
"View": "Ver",
|
||||||
|
"Format": "Formatar",
|
||||||
|
"Table": "Tabela",
|
||||||
|
"Tools": "Ferramentas",
|
||||||
|
"Powered by {0}": "Criado em {0}",
|
||||||
|
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Caixa de texto formatado. Pressione ALT-F9 para exibir o menu. Pressione ALT-F10 para exibir a barra de ferramentas. Pressione ALT-0 para exibir a ajuda",
|
||||||
|
"Image title": "T\u00edtulo da imagem",
|
||||||
|
"Border width": "Largura do limite",
|
||||||
|
"Border style": "Estilo do limite",
|
||||||
|
"Error": "Erro",
|
||||||
|
"Warn": "Aviso",
|
||||||
|
"Valid": "V\u00e1lido",
|
||||||
|
"To open the popup, press Shift+Enter": "Para abrir o pop-up, prima Shift+Enter",
|
||||||
|
"Rich Text Area. Press ALT-0 for help.": "\u00c1rea de texto formatado. Prima ALT-0 para exibir a ajuda.",
|
||||||
|
"System Font": "Tipo de letra do sistema",
|
||||||
|
"Failed to upload image: {0}": "Falha ao carregar imagem: {0}",
|
||||||
|
"Failed to load plugin: {0} from url {1}": "Falha ao carregar plugin: {0} do URL {1}",
|
||||||
|
"Failed to load plugin url: {0}": "Falha ao carregar o URL do plugin: {0}",
|
||||||
|
"Failed to initialize plugin: {0}": "Falha ao inicializar plugin: {0}",
|
||||||
|
"example": "exemplo",
|
||||||
|
"Search": "Pesquisar",
|
||||||
|
"All": "Tudo",
|
||||||
|
"Currency": "Moeda",
|
||||||
|
"Text": "Texto",
|
||||||
|
"Quotations": "Aspas",
|
||||||
|
"Mathematical": "Matem\u00e1tico",
|
||||||
|
"Extended Latin": "Carateres latinos estendidos",
|
||||||
|
"Symbols": "S\u00edmbolos",
|
||||||
|
"Arrows": "Setas",
|
||||||
|
"User Defined": "Definido pelo utilizador",
|
||||||
|
"dollar sign": "cifr\u00e3o",
|
||||||
|
"currency sign": "sinal monet\u00e1rio",
|
||||||
|
"euro-currency sign": "sinal monet\u00e1rio do euro",
|
||||||
|
"colon sign": "sinal de dois pontos",
|
||||||
|
"cruzeiro sign": "sinal de cruzeiro",
|
||||||
|
"french franc sign": "sinal de franco franc\u00eas",
|
||||||
|
"lira sign": "sinal de lira",
|
||||||
|
"mill sign": "sinal de por mil",
|
||||||
|
"naira sign": "sinal de naira",
|
||||||
|
"peseta sign": "sinal de peseta",
|
||||||
|
"rupee sign": "sinal de r\u00fapia",
|
||||||
|
"won sign": "sinal de won",
|
||||||
|
"new sheqel sign": "sinal de novo sheqel",
|
||||||
|
"dong sign": "sinal de dong",
|
||||||
|
"kip sign": "sinal kip",
|
||||||
|
"tugrik sign": "sinal tugrik",
|
||||||
|
"drachma sign": "sinal drachma",
|
||||||
|
"german penny symbol": "sinal de penny alem\u00e3o",
|
||||||
|
"peso sign": "sinal de peso",
|
||||||
|
"guarani sign": "sinal de guarani",
|
||||||
|
"austral sign": "sinal de austral",
|
||||||
|
"hryvnia sign": "sinal hryvnia",
|
||||||
|
"cedi sign": "sinal de cedi",
|
||||||
|
"livre tournois sign": "sinal de libra de tours",
|
||||||
|
"spesmilo sign": "sinal de spesmilo",
|
||||||
|
"tenge sign": "sinal de tengue",
|
||||||
|
"indian rupee sign": "sinal de rupia indiana",
|
||||||
|
"turkish lira sign": "sinal de lira turca",
|
||||||
|
"nordic mark sign": "sinal de marca n\u00f3rdica",
|
||||||
|
"manat sign": "sinal manat",
|
||||||
|
"ruble sign": "sinal de rublo",
|
||||||
|
"yen character": "sinal de iene",
|
||||||
|
"yuan character": "sinal de iuane",
|
||||||
|
"yuan character, in hong kong and taiwan": "sinal de iuane, em Hong Kong e Taiwan",
|
||||||
|
"yen\/yuan character variant one": "variante um de sinal de iene\/iuane",
|
||||||
|
"Loading emoticons...": "A carregar \u00edcones expressivos...",
|
||||||
|
"Could not load emoticons": "N\u00e3o foi poss\u00edvel carregar \u00edcones expressivos",
|
||||||
|
"People": "Pessoas",
|
||||||
|
"Animals and Nature": "Animais e natureza",
|
||||||
|
"Food and Drink": "Comida e bebida",
|
||||||
|
"Activity": "Atividade",
|
||||||
|
"Travel and Places": "Viagens e lugares",
|
||||||
|
"Objects": "Objetos",
|
||||||
|
"Flags": "Bandeiras",
|
||||||
|
"Characters": "Carateres",
|
||||||
|
"Characters (no spaces)": "Carateres (sem espa\u00e7os)",
|
||||||
|
"{0} characters": "{0} carateres",
|
||||||
|
"Error: Form submit field collision.": "Erro: conflito no campo de submiss\u00e3o de formul\u00e1rio.",
|
||||||
|
"Error: No form element found.": "Erro: nenhum elemento de formul\u00e1rio encontrado.",
|
||||||
|
"Update": "Atualizar",
|
||||||
|
"Color swatch": "Cole\u00e7\u00e3o de cores",
|
||||||
|
"Turquoise": "Turquesa",
|
||||||
|
"Green": "Verde",
|
||||||
|
"Blue": "Azul",
|
||||||
|
"Purple": "P\u00farpura",
|
||||||
|
"Navy Blue": "Azul-atl\u00e2ntico",
|
||||||
|
"Dark Turquoise": "Turquesa escuro",
|
||||||
|
"Dark Green": "Verde escuro",
|
||||||
|
"Medium Blue": "Azul interm\u00e9dio",
|
||||||
|
"Medium Purple": "P\u00farpura interm\u00e9dio",
|
||||||
|
"Midnight Blue": "Azul muito escuro",
|
||||||
|
"Yellow": "Amarelo",
|
||||||
|
"Orange": "Laranja",
|
||||||
|
"Red": "Vermelho",
|
||||||
|
"Light Gray": "Cinzento claro",
|
||||||
|
"Gray": "Cinzento",
|
||||||
|
"Dark Yellow": "Amarelo escuro",
|
||||||
|
"Dark Orange": "Laranja escuro",
|
||||||
|
"Dark Red": "Vermelho escuro",
|
||||||
|
"Medium Gray": "Cinzento m\u00e9dio",
|
||||||
|
"Dark Gray": "Cinzento escuro",
|
||||||
|
"Light Green": "Verde claro",
|
||||||
|
"Light Yellow": "Amarelo claro",
|
||||||
|
"Light Red": "Vermelho claro",
|
||||||
|
"Light Purple": "P\u00farpura claro",
|
||||||
|
"Light Blue": "Azul claro",
|
||||||
|
"Dark Purple": "P\u00farpura escuro",
|
||||||
|
"Dark Blue": "Azul escuro",
|
||||||
|
"Black": "Preto",
|
||||||
|
"White": "Branco",
|
||||||
|
"Switch to or from fullscreen mode": "Entrar ou sair do modo de ecr\u00e3 inteiro",
|
||||||
|
"Open help dialog": "Abrir caixa de di\u00e1logo Ajuda",
|
||||||
|
"history": "hist\u00f3rico",
|
||||||
|
"styles": "estilos",
|
||||||
|
"formatting": "formata\u00e7\u00e3o",
|
||||||
|
"alignment": "alinhamento",
|
||||||
|
"indentation": "avan\u00e7o",
|
||||||
|
"Font": "Tipo de letra",
|
||||||
|
"Size": "Tamanho",
|
||||||
|
"More...": "Mais...",
|
||||||
|
"Select...": "Selecionar...",
|
||||||
|
"Preferences": "Prefer\u00eancias",
|
||||||
|
"Yes": "Sim",
|
||||||
|
"No": "N\u00e3o",
|
||||||
|
"Keyboard Navigation": "Navega\u00e7\u00e3o com teclado",
|
||||||
|
"Version": "Vers\u00e3o",
|
||||||
|
"Code view": "Vista do c\u00f3digo-fonte",
|
||||||
|
"Open popup menu for split buttons": "Abrir o menu popup para bot\u00f5es divididos",
|
||||||
|
"List Properties": "Propriedades da lista",
|
||||||
|
"List properties...": "Propriedades da lista\u2026",
|
||||||
|
"Start list at number": "Come\u00e7ar a lista pelo n\u00famero",
|
||||||
|
"Line height": "Altura da linha",
|
||||||
|
"comments": "coment\u00e1rios",
|
||||||
|
"Format Painter": "Pincel de formata\u00e7\u00e3o",
|
||||||
|
"Insert\/edit iframe": "Inserir\/editar iframe",
|
||||||
|
"Capitalization": "Capitaliza\u00e7\u00e3o",
|
||||||
|
"lowercase": "min\u00fasculas",
|
||||||
|
"UPPERCASE": "MAI\u00daSCULAS",
|
||||||
|
"Title Case": "Iniciais mai\u00fasculas",
|
||||||
|
"permanent pen": "caneta permanente",
|
||||||
|
"Permanent Pen Properties": "Propriedades da Caneta Permanente",
|
||||||
|
"Permanent pen properties...": "Propriedades da caneta permanente...",
|
||||||
|
"case change": "mudan\u00e7a de capitaliza\u00e7\u00e3o",
|
||||||
|
"page embed": "incorporar p\u00e1gina",
|
||||||
|
"Advanced sort...": "Ordena\u00e7\u00e3o avan\u00e7ada\u2026",
|
||||||
|
"Advanced Sort": "Ordena\u00e7\u00e3o avan\u00e7ada",
|
||||||
|
"Sort table by column ascending": "Ordenar tabela por coluna ascendente",
|
||||||
|
"Sort table by column descending": "Ordenar tabela por coluna descendente",
|
||||||
|
"Sort": "Ordenar",
|
||||||
|
"Order": "Ordem",
|
||||||
|
"Sort by": "Ordenar por",
|
||||||
|
"Ascending": "Ascendente",
|
||||||
|
"Descending": "Descendente",
|
||||||
|
"Column {0}": "Coluna {0}",
|
||||||
|
"Row {0}": "Linha {0}",
|
||||||
|
"Spellcheck...": "Verifica\u00e7\u00e3o ortogr\u00e1fica...",
|
||||||
|
"Misspelled word": "Palavra mal escrita",
|
||||||
|
"Suggestions": "Sugest\u00f5es",
|
||||||
|
"Change": "Alterar",
|
||||||
|
"Finding word suggestions": "Encontrar sugest\u00f5es de palavras",
|
||||||
|
"Success": "Sucesso",
|
||||||
|
"Repair": "Reparar",
|
||||||
|
"Issue {0} of {1}": "Problema {0} de {1}",
|
||||||
|
"Images must be marked as decorative or have an alternative text description": "As imagens devem ser marcadas como decorativas ou ter uma descri\u00e7\u00e3o textual alternativa",
|
||||||
|
"Images must have an alternative text description. Decorative images are not allowed.": "As imagens devem ter uma descri\u00e7\u00e3o textual alternativa. N\u00e3o s\u00e3o permitidas imagens meramente decorativas.",
|
||||||
|
"Or provide alternative text:": "Ou forne\u00e7a um texto alternativo:",
|
||||||
|
"Make image decorative:": "Marque a imagem como decorativa:",
|
||||||
|
"ID attribute must be unique": "O atributo ID tem de ser \u00fanico",
|
||||||
|
"Make ID unique": "Tornar o ID \u00fanico",
|
||||||
|
"Keep this ID and remove all others": "Mantenha este ID e remova todos os outros",
|
||||||
|
"Remove this ID": "Remover este ID",
|
||||||
|
"Remove all IDs": "Remover todos os IDs",
|
||||||
|
"Checklist": "Lista de verifica\u00e7\u00e3o",
|
||||||
|
"Anchor": "\u00c2ncora",
|
||||||
|
"Special character": "Car\u00e1cter especial",
|
||||||
|
"Code sample": "Amostra de c\u00f3digo",
|
||||||
|
"Color": "Cor",
|
||||||
|
"Document properties": "Propriedades do documento",
|
||||||
|
"Image description": "Descri\u00e7\u00e3o da imagem",
|
||||||
|
"Image": "Imagem",
|
||||||
|
"Insert link": "Inserir liga\u00e7\u00e3o",
|
||||||
|
"Target": "Alvo",
|
||||||
|
"Link": "Liga\u00e7\u00e3o",
|
||||||
|
"Poster": "Autor",
|
||||||
|
"Media": "Media",
|
||||||
|
"Print": "Imprimir",
|
||||||
|
"Prev": "Anterior",
|
||||||
|
"Find and replace": "Pesquisar e substituir",
|
||||||
|
"Whole words": "Palavras completas",
|
||||||
|
"Insert template": "Inserir modelo"
|
||||||
|
});
|
@ -36,7 +36,7 @@ function init(logType) {
|
|||||||
d.innerHTML = "loading ...";
|
d.innerHTML = "loading ...";
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: window.location.pathname + "/../../ajax/log/" + logType,
|
url: getPath() + "/ajax/log/" + logType,
|
||||||
datatype: "text",
|
datatype: "text",
|
||||||
cache: false
|
cache: false
|
||||||
})
|
})
|
||||||
|
@ -85,14 +85,6 @@ $(document).on("change", "select[data-controlall]", function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*$(document).on("click", "#sendbtn", function (event) {
|
|
||||||
postButton(event, $(this).data('action'));
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("click", ".sendbutton", function (event) {
|
|
||||||
// $(".sendbutton").on("click", "body", function(event) {
|
|
||||||
postButton(event, $(this).data('action'));
|
|
||||||
});*/
|
|
||||||
|
|
||||||
$(document).on("click", ".postAction", function (event) {
|
$(document).on("click", ".postAction", function (event) {
|
||||||
// $(".sendbutton").on("click", "body", function(event) {
|
// $(".sendbutton").on("click", "body", function(event) {
|
||||||
@ -100,7 +92,6 @@ $(document).on("click", ".postAction", function (event) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Syntax has to be bind not on, otherwise problems with firefox
|
// Syntax has to be bind not on, otherwise problems with firefox
|
||||||
$(".container-fluid").bind("dragenter dragover", function () {
|
$(".container-fluid").bind("dragenter dragover", function () {
|
||||||
if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
|
if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
|
||||||
@ -313,7 +304,7 @@ $(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fillFileTable(path, type, folder, filt) {
|
function fillFileTable(path, type, folder, filt) {
|
||||||
var request_path = "/../../ajax/pathchooser/";
|
var request_path = "/ajax/pathchooser/";
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: {
|
data: {
|
||||||
@ -321,7 +312,7 @@ $(function() {
|
|||||||
folder: folder,
|
folder: folder,
|
||||||
filter: filt
|
filter: filt
|
||||||
},
|
},
|
||||||
url: window.location.pathname + request_path,
|
url: getPath() + request_path,
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
if ($("#element_selected").text() ==="") {
|
if ($("#element_selected").text() ==="") {
|
||||||
$("#element_selected").text(data.cwd);
|
$("#element_selected").text(data.cwd);
|
||||||
@ -342,7 +333,6 @@ $(function() {
|
|||||||
} else {
|
} else {
|
||||||
$("#parent").addClass('hidden')
|
$("#parent").addClass('hidden')
|
||||||
}
|
}
|
||||||
// console.log(data);
|
|
||||||
data.files.forEach(function(entry) {
|
data.files.forEach(function(entry) {
|
||||||
if(entry.type === "dir") {
|
if(entry.type === "dir") {
|
||||||
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
||||||
@ -364,12 +354,6 @@ $(function() {
|
|||||||
layoutMode : "fitRows"
|
layoutMode : "fitRows"
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".grid").isotope({
|
|
||||||
// options
|
|
||||||
itemSelector : ".grid-item",
|
|
||||||
layoutMode : "fitColumns"
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($(".load-more").length && $(".next").length) {
|
if ($(".load-more").length && $(".next").length) {
|
||||||
var $loadMore = $(".load-more .row").infiniteScroll({
|
var $loadMore = $(".load-more .row").infiniteScroll({
|
||||||
debug: false,
|
debug: false,
|
||||||
@ -440,7 +424,7 @@ $(function() {
|
|||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../get_update_status",
|
url: getPath() + "/get_update_status",
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
$this.html(buttonText);
|
$this.html(buttonText);
|
||||||
|
|
||||||
@ -544,6 +528,7 @@ $(function() {
|
|||||||
$("#bookDetailsModal")
|
$("#bookDetailsModal")
|
||||||
.on("show.bs.modal", function(e) {
|
.on("show.bs.modal", function(e) {
|
||||||
$("#flash_danger").remove();
|
$("#flash_danger").remove();
|
||||||
|
$("#flash_success").remove();
|
||||||
var $modalBody = $(this).find(".modal-body");
|
var $modalBody = $(this).find(".modal-body");
|
||||||
|
|
||||||
// Prevent static assets from loading multiple times
|
// Prevent static assets from loading multiple times
|
||||||
@ -656,7 +641,6 @@ $(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#user_submit").click(function() {
|
$("#user_submit").click(function() {
|
||||||
this.closest("form").submit();
|
this.closest("form").submit();
|
||||||
});
|
});
|
||||||
@ -688,7 +672,7 @@ $(function() {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/simulatedbchange",
|
url: getPath() + "/ajax/simulatedbchange",
|
||||||
data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
|
data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
if ( data.change ) {
|
if ( data.change ) {
|
||||||
@ -715,17 +699,16 @@ $(function() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.blur();
|
this.blur();
|
||||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||||
var request_path = "/../../admin/ajaxconfig";
|
var request_path = "/admin/ajaxconfig";
|
||||||
var loader = "/../..";
|
|
||||||
$("#flash_success").remove();
|
$("#flash_success").remove();
|
||||||
$("#flash_danger").remove();
|
$("#flash_danger").remove();
|
||||||
$.post(window.location.pathname + request_path, $(this).closest("form").serialize(), function(data) {
|
$.post(getPath() + request_path, $(this).closest("form").serialize(), function(data) {
|
||||||
$('#config_upload_formats').val(data.config_upload);
|
$('#config_upload_formats').val(data.config_upload);
|
||||||
if(data.reboot) {
|
if(data.reboot) {
|
||||||
$("#spinning_success").show();
|
$("#spinning_success").show();
|
||||||
var rebootInterval = setInterval(function(){
|
var rebootInterval = setInterval(function(){
|
||||||
$.get({
|
$.get({
|
||||||
url:window.location.pathname + "/../../admin/alive",
|
url:getPath() + "/admin/alive",
|
||||||
success: function (d, statusText, xhr) {
|
success: function (d, statusText, xhr) {
|
||||||
if (xhr.status < 400) {
|
if (xhr.status < 400) {
|
||||||
$("#spinning_success").hide();
|
$("#spinning_success").hide();
|
||||||
@ -751,7 +734,6 @@ $(function() {
|
|||||||
$(this).data('value'),
|
$(this).data('value'),
|
||||||
function(value){
|
function(value){
|
||||||
postButton(event, $("#delete_shelf").data("action"));
|
postButton(event, $("#delete_shelf").data("action"));
|
||||||
// $("#delete_shelf").closest("form").submit()
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
65
cps/static/js/password.js
Normal file
65
cps/static/js/password.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
* Copyright (C) 2022 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/>.
|
||||||
|
*/
|
||||||
|
$(document).ready(function() {
|
||||||
|
i18next.use(i18nextHttpBackend).init({
|
||||||
|
lng: $('#password').data("lang"),
|
||||||
|
debug: false,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
backend: {
|
||||||
|
loadPath: getPath() + "/static/js/libs/pwstrength/locales/{{lng}}.json",
|
||||||
|
},
|
||||||
|
|
||||||
|
}, function () {
|
||||||
|
if ($('#password').data("verify")) {
|
||||||
|
// Initialized and ready to go
|
||||||
|
var options = {};
|
||||||
|
options.common = {
|
||||||
|
minChar: $('#password').data("min"),
|
||||||
|
maxChar: -1
|
||||||
|
}
|
||||||
|
options.ui = {
|
||||||
|
bootstrap3: true,
|
||||||
|
showProgressBar: false,
|
||||||
|
showErrors: true,
|
||||||
|
showVerdicts: false,
|
||||||
|
}
|
||||||
|
options.rules= {
|
||||||
|
specialCharClass: "(?=.*?[^A-Za-z\\s0-9])",
|
||||||
|
activated: {
|
||||||
|
wordNotEmail: false,
|
||||||
|
wordMinLength: $('#password').data("min"),
|
||||||
|
// wordMaxLength: false,
|
||||||
|
// wordInvalidChar: true,
|
||||||
|
wordSimilarToUsername: false,
|
||||||
|
wordSequences: false,
|
||||||
|
wordTwoCharacterClasses: false,
|
||||||
|
wordRepetitions: false,
|
||||||
|
wordLowercase: $('#password').data("lower") === "True" ? true : false,
|
||||||
|
wordUppercase: $('#password').data("upper") === "True" ? true : false,
|
||||||
|
wordOneNumber: $('#password').data("number") === "True" ? true : false,
|
||||||
|
wordThreeNumbers: false,
|
||||||
|
wordOneSpecialChar: $('#password').data("special") === "True" ? true : false,
|
||||||
|
// wordTwoSpecialChar: true,
|
||||||
|
wordUpperLowerCombo: false,
|
||||||
|
wordLetterNumberCombo: false,
|
||||||
|
wordLetterNumberCharCombo: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('#password').pwstrength(options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -49,7 +49,7 @@ $(function() {
|
|||||||
method: "post",
|
method: "post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../ajax/canceltask",
|
url: getPath() + "/ajax/canceltask",
|
||||||
data: JSON.stringify({"task_id": taskId}),
|
data: JSON.stringify({"task_id": taskId}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -634,7 +634,7 @@ function UserActions (value, row) {
|
|||||||
|
|
||||||
/* Function for cancelling tasks */
|
/* Function for cancelling tasks */
|
||||||
function TaskActions (value, row) {
|
function TaskActions (value, row) {
|
||||||
var cancellableStats = [0, 1, 2];
|
var cancellableStats = [0, 2];
|
||||||
if (row.task_id && row.is_cancellable && cancellableStats.includes(row.stat)) {
|
if (row.task_id && row.is_cancellable && cancellableStats.includes(row.stat)) {
|
||||||
return [
|
return [
|
||||||
"<div class=\"danger task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.task_id + "\" title=\"Cancel\">",
|
"<div class=\"danger task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.task_id + "\" title=\"Cancel\">",
|
||||||
|
@ -64,13 +64,13 @@ class TaskConvert(CalibreTask):
|
|||||||
if df:
|
if df:
|
||||||
datafile = os.path.join(config.config_calibre_dir,
|
datafile = os.path.join(config.config_calibre_dir,
|
||||||
cur_book.path,
|
cur_book.path,
|
||||||
data.name + u"." + self.settings['old_book_format'].lower())
|
data.name + "." + self.settings['old_book_format'].lower())
|
||||||
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
||||||
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
||||||
df.GetContentFile(datafile)
|
df.GetContentFile(datafile)
|
||||||
worker_db.session.close()
|
worker_db.session.close()
|
||||||
else:
|
else:
|
||||||
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
error_message = _("%(format)s not found on Google Drive: %(fn)s",
|
||||||
format=self.settings['old_book_format'],
|
format=self.settings['old_book_format'],
|
||||||
fn=data.name + "." + self.settings['old_book_format'].lower())
|
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||||
worker_db.session.close()
|
worker_db.session.close()
|
||||||
@ -78,7 +78,7 @@ class TaskConvert(CalibreTask):
|
|||||||
|
|
||||||
filename = self._convert_ebook_format()
|
filename = self._convert_ebook_format()
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
os.remove(self.file_path + u'.' + self.settings['old_book_format'].lower())
|
os.remove(self.file_path + '.' + self.settings['old_book_format'].lower())
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
@ -89,7 +89,7 @@ class TaskConvert(CalibreTask):
|
|||||||
# if we're sending to E-Reader after converting, create a one-off task and run it immediately
|
# if we're sending to E-Reader after converting, create a one-off task and run it immediately
|
||||||
# todo: figure out how to incorporate this into the progress
|
# todo: figure out how to incorporate this into the progress
|
||||||
try:
|
try:
|
||||||
EmailText = N_(u"%(book)s send to E-Reader", book=escape(self.title))
|
EmailText = N_("%(book)s send to E-Reader", book=escape(self.title))
|
||||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||||
self.results["path"],
|
self.results["path"],
|
||||||
filename,
|
filename,
|
||||||
@ -107,8 +107,8 @@ class TaskConvert(CalibreTask):
|
|||||||
local_db = db.CalibreDB(expire_on_commit=False, init=True)
|
local_db = db.CalibreDB(expire_on_commit=False, init=True)
|
||||||
file_path = self.file_path
|
file_path = self.file_path
|
||||||
book_id = self.book_id
|
book_id = self.book_id
|
||||||
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
format_old_ext = '.' + self.settings['old_book_format'].lower()
|
||||||
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
format_new_ext = '.' + self.settings['new_book_format'].lower()
|
||||||
|
|
||||||
# check to see if destination format already exists - or if book is in database
|
# check to see if destination format already exists - or if book is in database
|
||||||
# if it does - mark the conversion task as complete and return a success
|
# if it does - mark the conversion task as complete and return a success
|
||||||
@ -133,7 +133,7 @@ class TaskConvert(CalibreTask):
|
|||||||
local_db.session.rollback()
|
local_db.session.rollback()
|
||||||
log.error("Database error: %s", e)
|
log.error("Database error: %s", e)
|
||||||
local_db.session.close()
|
local_db.session.close()
|
||||||
self._handleError(N_("Database error: %(error)s.", error=e))
|
self._handleError(N_("Oops! Database Error: %(error)s.", error=e))
|
||||||
return
|
return
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
local_db.session.close()
|
local_db.session.close()
|
||||||
@ -150,7 +150,7 @@ class TaskConvert(CalibreTask):
|
|||||||
else:
|
else:
|
||||||
# check if calibre converter-executable is existing
|
# check if calibre converter-executable is existing
|
||||||
if not os.path.exists(config.config_converterpath):
|
if not os.path.exists(config.config_converterpath):
|
||||||
self._handleError(N_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
self._handleError(N_("Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
||||||
return
|
return
|
||||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ class TaskConvert(CalibreTask):
|
|||||||
try:
|
try:
|
||||||
p = process_open(command, quotes)
|
p = process_open(command, quotes)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return 1, N_(u"Kepubify-converter failed: %(error)s", error=e)
|
return 1, N_("Kepubify-converter failed: %(error)s", error=e)
|
||||||
self.progress = 0.01
|
self.progress = 0.01
|
||||||
while True:
|
while True:
|
||||||
nextline = p.stdout.readlines()
|
nextline = p.stdout.readlines()
|
||||||
@ -220,7 +220,7 @@ class TaskConvert(CalibreTask):
|
|||||||
copyfile(converted_file[0], (file_path + format_new_ext))
|
copyfile(converted_file[0], (file_path + format_new_ext))
|
||||||
os.unlink(converted_file[0])
|
os.unlink(converted_file[0])
|
||||||
else:
|
else:
|
||||||
return 1, N_(u"Converted file not found or more than one file in folder %(folder)s",
|
return 1, N_("Converted file not found or more than one file in folder %(folder)s",
|
||||||
folder=os.path.dirname(file_path))
|
folder=os.path.dirname(file_path))
|
||||||
return check, None
|
return check, None
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ class TaskConvert(CalibreTask):
|
|||||||
|
|
||||||
p = process_open(command, quotes, newlines=False)
|
p = process_open(command, quotes, newlines=False)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return 1, N_(u"Ebook-converter failed: %(error)s", error=e)
|
return 1, N_("Ebook-converter failed: %(error)s", error=e)
|
||||||
|
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
nextline = p.stdout.readline()
|
nextline = p.stdout.readline()
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
import socket
|
import socket
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@ -152,7 +153,7 @@ class TaskEmail(CalibreTask):
|
|||||||
main_type, sub_type = content_type.split('/', 1)
|
main_type, sub_type = content_type.split('/', 1)
|
||||||
message.add_attachment(data, maintype=main_type, subtype=sub_type, filename=self.attachment)
|
message.add_attachment(data, maintype=main_type, subtype=sub_type, filename=self.attachment)
|
||||||
else:
|
else:
|
||||||
self._handleError(u"Attachment not found")
|
self._handleError("Attachment not found")
|
||||||
return
|
return
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ class TaskEmail(CalibreTask):
|
|||||||
self.send_gmail_email(msg)
|
self.send_gmail_email(msg)
|
||||||
except MemoryError as e:
|
except MemoryError as e:
|
||||||
log.error_or_exception(e, stacklevel=3)
|
log.error_or_exception(e, stacklevel=3)
|
||||||
self._handleError(u'MemoryError sending e-mail: {}'.format(str(e)))
|
self._handleError('MemoryError sending e-mail: {}'.format(str(e)))
|
||||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||||
log.error_or_exception(e, stacklevel=3)
|
log.error_or_exception(e, stacklevel=3)
|
||||||
if hasattr(e, "smtp_error"):
|
if hasattr(e, "smtp_error"):
|
||||||
@ -177,13 +178,13 @@ class TaskEmail(CalibreTask):
|
|||||||
text = '\n'.join(e.args)
|
text = '\n'.join(e.args)
|
||||||
else:
|
else:
|
||||||
text = ''
|
text = ''
|
||||||
self._handleError(u'Smtplib Error sending e-mail: {}'.format(text))
|
self._handleError('Smtplib Error sending e-mail: {}'.format(text))
|
||||||
except (socket.error) as e:
|
except (socket.error) as e:
|
||||||
log.error_or_exception(e, stacklevel=3)
|
log.error_or_exception(e, stacklevel=3)
|
||||||
self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror))
|
self._handleError('Socket Error sending e-mail: {}'.format(e.strerror))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.error_or_exception(ex, stacklevel=3)
|
log.error_or_exception(ex, stacklevel=3)
|
||||||
self._handleError(u'Error sending e-mail: {}'.format(ex))
|
self._handleError('Error sending e-mail: {}'.format(ex))
|
||||||
|
|
||||||
def send_standard_email(self, msg):
|
def send_standard_email(self, msg):
|
||||||
use_ssl = int(self.settings.get('mail_use_ssl', 0))
|
use_ssl = int(self.settings.get('mail_use_ssl', 0))
|
||||||
@ -192,8 +193,9 @@ class TaskEmail(CalibreTask):
|
|||||||
# on python3 debugoutput is caught with overwritten _print_debug function
|
# on python3 debugoutput is caught with overwritten _print_debug function
|
||||||
log.debug("Start sending e-mail")
|
log.debug("Start sending e-mail")
|
||||||
if use_ssl == 2:
|
if use_ssl == 2:
|
||||||
|
context = ssl.create_default_context()
|
||||||
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
|
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
|
||||||
timeout=timeout)
|
timeout=timeout, context=context)
|
||||||
else:
|
else:
|
||||||
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)
|
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)
|
||||||
|
|
||||||
@ -201,9 +203,10 @@ class TaskEmail(CalibreTask):
|
|||||||
if logger.is_debug_enabled():
|
if logger.is_debug_enabled():
|
||||||
self.asyncSMTP.set_debuglevel(1)
|
self.asyncSMTP.set_debuglevel(1)
|
||||||
if use_ssl == 1:
|
if use_ssl == 1:
|
||||||
self.asyncSMTP.starttls()
|
context = ssl.create_default_context()
|
||||||
if self.settings["mail_password"]:
|
self.asyncSMTP.starttls(context=context)
|
||||||
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"]))
|
if self.settings["mail_password_e"]:
|
||||||
|
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password_e"]))
|
||||||
|
|
||||||
# Convert message to something to send
|
# Convert message to something to send
|
||||||
fp = StringIO()
|
fp = StringIO()
|
||||||
@ -257,7 +260,7 @@ class TaskEmail(CalibreTask):
|
|||||||
file_.close()
|
file_.close()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
log.error_or_exception(e, stacklevel=3)
|
log.error_or_exception(e, stacklevel=3)
|
||||||
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
log.error('The requested file could not be read. Maybe wrong permissions?')
|
||||||
return None
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -17,13 +17,12 @@
|
|||||||
# 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 lxml import objectify
|
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from html import escape
|
|
||||||
|
|
||||||
from cps import config, db, fs, gdriveutils, logger, ub
|
|
||||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
from cps import config, db, gdriveutils, logger
|
||||||
|
from cps.services.worker import CalibreTask
|
||||||
from flask_babel import lazy_gettext as N_
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
OPF_NAMESPACE = "http://www.idpf.org/2007/opf"
|
OPF_NAMESPACE = "http://www.idpf.org/2007/opf"
|
||||||
@ -74,7 +73,10 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
def backup_metadata(self):
|
def backup_metadata(self):
|
||||||
try:
|
try:
|
||||||
metadata_backup = self.calibre_db.session.query(db.Metadata_Dirtied).all()
|
metadata_backup = self.calibre_db.session.query(db.Metadata_Dirtied).all()
|
||||||
custom_columns = self.calibre_db.session.query(db.CustomColumns).order_by(db.CustomColumns.label).all()
|
custom_columns = (self.calibre_db.session.query(db.CustomColumns)
|
||||||
|
.filter(db.CustomColumns.mark_for_delete == 0)
|
||||||
|
.filter(db.CustomColumns.datatype.notin_(db.cc_exceptions))
|
||||||
|
.order_by(db.CustomColumns.label).all())
|
||||||
count = len(metadata_backup)
|
count = len(metadata_backup)
|
||||||
i = 0
|
i = 0
|
||||||
for backup in metadata_backup:
|
for backup in metadata_backup:
|
||||||
@ -86,7 +88,6 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
self.open_metadata(book, custom_columns)
|
self.open_metadata(book, custom_columns)
|
||||||
else:
|
else:
|
||||||
self.log.error("Book {} not found in database".format(backup.book))
|
self.log.error("Book {} not found in database".format(backup.book))
|
||||||
# self._handleError("Book {} not found in database".format(backup.book))
|
|
||||||
i += 1
|
i += 1
|
||||||
self.progress = (1.0 / count) * i
|
self.progress = (1.0 / count) * i
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
@ -100,50 +101,29 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
self.calibre_db.session.close()
|
self.calibre_db.session.close()
|
||||||
|
|
||||||
def open_metadata(self, book, custom_columns):
|
def open_metadata(self, book, custom_columns):
|
||||||
|
package = self.create_new_metadata_backup(book, custom_columns)
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
if not gdriveutils.is_gdrive_ready():
|
if not gdriveutils.is_gdrive_ready():
|
||||||
raise Exception('Google Drive is configured but not ready')
|
raise Exception('Google Drive is configured but not ready')
|
||||||
|
|
||||||
web_content_link = gdriveutils.get_metadata_backup_via_gdrive(book.path)
|
gdriveutils.uploadFileToEbooksFolder(os.path.join(book.path, 'metadata.opf').replace("\\", "/"),
|
||||||
if not web_content_link:
|
etree.tostring(package,
|
||||||
raise Exception('Google Drive cover url not found')
|
xml_declaration=True,
|
||||||
|
encoding='utf-8',
|
||||||
stream = None
|
pretty_print=True).decode('utf-8'),
|
||||||
try:
|
True)
|
||||||
stream = urlopen(web_content_link)
|
|
||||||
except Exception as ex:
|
|
||||||
# Bubble exception to calling function
|
|
||||||
self.log.debug('Error reading metadata.opf: ' + str(ex)) # ToDo Check whats going on
|
|
||||||
raise ex
|
|
||||||
finally:
|
|
||||||
if stream is not None:
|
|
||||||
stream.close()
|
|
||||||
else:
|
else:
|
||||||
# ToDo: Handle book folder not found or not readable
|
# ToDo: Handle book folder not found or not readable
|
||||||
book_metadata_filepath = os.path.join(config.config_calibre_dir, book.path, 'metadata.opf')
|
book_metadata_filepath = os.path.join(config.config_calibre_dir, book.path, 'metadata.opf')
|
||||||
#if not os.path.isfile(book_metadata_filepath):
|
# prepare finalize everything and output
|
||||||
self.create_new_metadata_backup(book, custom_columns, book_metadata_filepath)
|
doc = etree.ElementTree(package)
|
||||||
# else:
|
try:
|
||||||
'''namespaces = {'dc': PURL_NAMESPACE, 'opf': OPF_NAMESPACE}
|
with open(book_metadata_filepath, 'wb') as f:
|
||||||
test = etree.parse(book_metadata_filepath)
|
doc.write(f, xml_declaration=True, encoding='utf-8', pretty_print=True)
|
||||||
root = test.getroot()
|
except Exception as ex:
|
||||||
for i in root.iter():
|
raise Exception('Writing Metadata failed with error: {} '.format(ex))
|
||||||
self.log.info(i)
|
|
||||||
title = root.find("dc:metadata", namespaces)
|
|
||||||
pass
|
|
||||||
with open(book_metadata_filepath, "rb") as f:
|
|
||||||
xml = f.read()
|
|
||||||
|
|
||||||
root = objectify.fromstring(xml)
|
def create_new_metadata_backup(self, book, custom_columns):
|
||||||
# root.metadata['{http://purl.org/dc/elements/1.1/}title']
|
|
||||||
# root.metadata[PURL + 'title']
|
|
||||||
# getattr(root.metadata, PURL +'title')
|
|
||||||
# test = objectify.parse()
|
|
||||||
pass
|
|
||||||
# backup not found has to be created
|
|
||||||
#raise Exception('Book cover file not found')'''
|
|
||||||
|
|
||||||
def create_new_metadata_backup(self, book, custom_columns, book_metadata_filepath):
|
|
||||||
# generate root package element
|
# generate root package element
|
||||||
package = etree.Element(OPF + "package", nsmap=OPF_NS)
|
package = etree.Element(OPF + "package", nsmap=OPF_NS)
|
||||||
package.set("unique-identifier", "uuid_id")
|
package.set("unique-identifier", "uuid_id")
|
||||||
@ -171,10 +151,13 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
|
|
||||||
date = etree.SubElement(metadata, PURL + "date", nsmap=NSMAP)
|
date = etree.SubElement(metadata, PURL + "date", nsmap=NSMAP)
|
||||||
date.text = '{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(d=book.pubdate)
|
date.text = '{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(d=book.pubdate)
|
||||||
if book.comments:
|
if book.comments and book.comments[0].text:
|
||||||
for b in book.comments:
|
for b in book.comments:
|
||||||
description = etree.SubElement(metadata, PURL + "description", nsmap=NSMAP)
|
description = etree.SubElement(metadata, PURL + "description", nsmap=NSMAP)
|
||||||
description.text = b.text
|
description.text = b.text
|
||||||
|
for b in book.publishers:
|
||||||
|
publisher = etree.SubElement(metadata, PURL + "publisher", nsmap=NSMAP)
|
||||||
|
publisher.text = str(b.name)
|
||||||
if not book.languages:
|
if not book.languages:
|
||||||
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
|
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
|
||||||
language.text = self.export_language
|
language.text = self.export_language
|
||||||
@ -196,6 +179,10 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
etree.SubElement(metadata, "meta", name="calibre:series_index",
|
etree.SubElement(metadata, "meta", name="calibre:series_index",
|
||||||
content=str(book.series_index),
|
content=str(book.series_index),
|
||||||
nsmap=NSMAP)
|
nsmap=NSMAP)
|
||||||
|
if len(book.ratings) and book.ratings[0].rating > 0:
|
||||||
|
etree.SubElement(metadata, "meta", name="calibre:rating",
|
||||||
|
content=str(book.ratings[0].rating),
|
||||||
|
nsmap=NSMAP)
|
||||||
etree.SubElement(metadata, "meta", name="calibre:timestamp",
|
etree.SubElement(metadata, "meta", name="calibre:timestamp",
|
||||||
content='{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(
|
content='{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(
|
||||||
d=book.timestamp),
|
d=book.timestamp),
|
||||||
@ -209,8 +196,8 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
extra = None
|
extra = None
|
||||||
cc_entry = getattr(book, "custom_column_" + str(cc.id))
|
cc_entry = getattr(book, "custom_column_" + str(cc.id))
|
||||||
if cc_entry.__len__():
|
if cc_entry.__len__():
|
||||||
value = cc_entry[0].get("value")
|
value = [c.value for c in cc_entry] if cc.is_multiple else cc_entry[0].value
|
||||||
extra = cc_entry[0].get("extra")
|
extra = cc_entry[0].extra if hasattr(cc_entry[0], "extra") else None
|
||||||
etree.SubElement(metadata, "meta", name="calibre:user_metadata:#{}".format(cc.label),
|
etree.SubElement(metadata, "meta", name="calibre:user_metadata:#{}".format(cc.label),
|
||||||
content=cc.to_json(value, extra, sequence),
|
content=cc.to_json(value, extra, sequence),
|
||||||
nsmap=NSMAP)
|
nsmap=NSMAP)
|
||||||
@ -221,16 +208,8 @@ class TaskBackupMetadata(CalibreTask):
|
|||||||
guide = etree.SubElement(package, "guide")
|
guide = etree.SubElement(package, "guide")
|
||||||
etree.SubElement(guide, "reference", type="cover", title=self.translated_title, href="cover.jpg")
|
etree.SubElement(guide, "reference", type="cover", title=self.translated_title, href="cover.jpg")
|
||||||
|
|
||||||
# prepare finalize everything and output
|
return package
|
||||||
doc = etree.ElementTree(package)
|
|
||||||
# doc = etree.tostring(package, xml_declaration=True, encoding='utf-8', pretty_print=True) # .replace(b"&quot;", b""")
|
|
||||||
try:
|
|
||||||
with open(book_metadata_filepath, 'wb') as f:
|
|
||||||
# f.write(doc)
|
|
||||||
doc.write(f, xml_declaration=True, encoding='utf-8', pretty_print=True)
|
|
||||||
except Exception:
|
|
||||||
# ToDo: Folder not writeable error
|
|
||||||
pass
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "Metadata backup"
|
return "Metadata backup"
|
||||||
|
@ -17,6 +17,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 os
|
import os
|
||||||
|
from shutil import copyfile, copyfileobj
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from .. import constants
|
from .. import constants
|
||||||
@ -92,7 +93,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
|
|
||||||
if generated > 0:
|
if generated > 0:
|
||||||
total_generated += generated
|
total_generated += generated
|
||||||
self.message = N_(u'Generated %(count)s cover thumbnails', count=total_generated)
|
self.message = N_('Generated %(count)s cover thumbnails', count=total_generated)
|
||||||
|
|
||||||
# Check if job has been cancelled or ended
|
# Check if job has been cancelled or ended
|
||||||
if self.stat == STAT_CANCELLED:
|
if self.stat == STAT_CANCELLED:
|
||||||
@ -137,7 +138,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
|
|
||||||
# Replace outdated or missing thumbnails
|
# Replace outdated or missing thumbnails
|
||||||
for thumbnail in book_cover_thumbnails:
|
for thumbnail in book_cover_thumbnails:
|
||||||
if book.last_modified > thumbnail.generated_at:
|
if book.last_modified.replace(tzinfo=None) > thumbnail.generated_at:
|
||||||
generated += 1
|
generated += 1
|
||||||
self.update_book_cover_thumbnail(book, thumbnail)
|
self.update_book_cover_thumbnail(book, thumbnail)
|
||||||
|
|
||||||
@ -188,14 +189,18 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
try:
|
try:
|
||||||
stream = urlopen(web_content_link)
|
stream = urlopen(web_content_link)
|
||||||
with Image(file=stream) as img:
|
with Image(file=stream) as img:
|
||||||
|
filename = self.cache.get_cache_file_path(thumbnail.filename,
|
||||||
|
constants.CACHE_TYPE_THUMBNAILS)
|
||||||
height = get_resize_height(thumbnail.resolution)
|
height = get_resize_height(thumbnail.resolution)
|
||||||
if img.height > height:
|
if img.height > height:
|
||||||
width = get_resize_width(thumbnail.resolution, img.width, img.height)
|
width = get_resize_width(thumbnail.resolution, img.width, img.height)
|
||||||
img.resize(width=width, height=height, filter='lanczos')
|
img.resize(width=width, height=height, filter='lanczos')
|
||||||
img.format = thumbnail.format
|
img.format = thumbnail.format
|
||||||
filename = self.cache.get_cache_file_path(thumbnail.filename,
|
|
||||||
constants.CACHE_TYPE_THUMBNAILS)
|
|
||||||
img.save(filename=filename)
|
img.save(filename=filename)
|
||||||
|
else:
|
||||||
|
with open(filename, 'rb') as 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))
|
||||||
@ -210,12 +215,15 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
|
|
||||||
with Image(filename=book_cover_filepath) as img:
|
with Image(filename=book_cover_filepath) as img:
|
||||||
height = get_resize_height(thumbnail.resolution)
|
height = get_resize_height(thumbnail.resolution)
|
||||||
|
filename = self.cache.get_cache_file_path(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
||||||
if img.height > height:
|
if img.height > height:
|
||||||
width = get_resize_width(thumbnail.resolution, img.width, img.height)
|
width = get_resize_width(thumbnail.resolution, img.width, img.height)
|
||||||
img.resize(width=width, height=height, filter='lanczos')
|
img.resize(width=width, height=height, filter='lanczos')
|
||||||
img.format = thumbnail.format
|
img.format = thumbnail.format
|
||||||
filename = self.cache.get_cache_file_path(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
|
||||||
img.save(filename=filename)
|
img.save(filename=filename)
|
||||||
|
else:
|
||||||
|
# take cover as is
|
||||||
|
copyfile(book_cover_filepath, filename)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -43,9 +43,7 @@ def get_email_status_json():
|
|||||||
@login_required
|
@login_required
|
||||||
def get_tasks_status():
|
def get_tasks_status():
|
||||||
# if current user admin, show all email, otherwise only own emails
|
# if current user admin, show all email, otherwise only own emails
|
||||||
tasks = WorkerThread.get_instance().tasks
|
return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
|
||||||
answer = render_task_status(tasks)
|
|
||||||
return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
|
|
||||||
|
|
||||||
|
|
||||||
# helper function to apply localize status information in tasklist entries
|
# helper function to apply localize status information in tasklist entries
|
||||||
@ -61,19 +59,19 @@ def render_task_status(tasklist):
|
|||||||
# localize the task status
|
# localize the task status
|
||||||
if isinstance(task.stat, int):
|
if isinstance(task.stat, int):
|
||||||
if task.stat == STAT_WAITING:
|
if task.stat == STAT_WAITING:
|
||||||
ret['status'] = _(u'Waiting')
|
ret['status'] = _('Waiting')
|
||||||
elif task.stat == STAT_FAIL:
|
elif task.stat == STAT_FAIL:
|
||||||
ret['status'] = _(u'Failed')
|
ret['status'] = _('Failed')
|
||||||
elif task.stat == STAT_STARTED:
|
elif task.stat == STAT_STARTED:
|
||||||
ret['status'] = _(u'Started')
|
ret['status'] = _('Started')
|
||||||
elif task.stat == STAT_FINISH_SUCCESS:
|
elif task.stat == STAT_FINISH_SUCCESS:
|
||||||
ret['status'] = _(u'Finished')
|
ret['status'] = _('Finished')
|
||||||
elif task.stat == STAT_ENDED:
|
elif task.stat == STAT_ENDED:
|
||||||
ret['status'] = _(u'Ended')
|
ret['status'] = _('Ended')
|
||||||
elif task.stat == STAT_CANCELLED:
|
elif task.stat == STAT_CANCELLED:
|
||||||
ret['status'] = _(u'Cancelled')
|
ret['status'] = _('Cancelled')
|
||||||
else:
|
else:
|
||||||
ret['status'] = _(u'Unknown Status')
|
ret['status'] = _('Unknown Status')
|
||||||
|
|
||||||
ret['taskMessage'] = "{}: {}".format(task.name, task.message) if task.message else task.name
|
ret['taskMessage'] = "{}: {}".format(task.name, task.message) if task.message else task.name
|
||||||
ret['progress'] = "{} %".format(int(task.progress * 100))
|
ret['progress'] = "{} %".format(int(task.progress * 100))
|
||||||
|
47
cps/templates/admin.html
Executable file → Normal file
47
cps/templates/admin.html
Executable file → Normal file
@ -11,8 +11,8 @@
|
|||||||
<table class="table table-striped" id="table_user">
|
<table class="table table-striped" id="table_user">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{_('Username')}}</th>
|
<th>{{_('Username')}}</th>
|
||||||
<th>{{_('E-mail Address')}}</th>
|
<th>{{_('Email')}}</th>
|
||||||
<th>{{_('Send to E-Reader E-mail Address')}}</th>
|
<th>{{_('Send to eReader Email')}}</th>
|
||||||
<th>{{_('Downloads')}}</th>
|
<th>{{_('Downloads')}}</th>
|
||||||
<th class="hidden-xs ">{{_('Admin')}}</th>
|
<th class="hidden-xs ">{{_('Admin')}}</th>
|
||||||
<th class="hidden-xs hidden-sm">{{_('Password')}}</th>
|
<th class="hidden-xs hidden-sm">{{_('Password')}}</th>
|
||||||
@ -59,45 +59,45 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>{{_('E-mail Server Settings')}}</h2>
|
<h2>{{_('Email Server Settings')}}</h2>
|
||||||
{% if config.get_mail_server_configured() %}
|
{% if config.get_mail_server_configured() %}
|
||||||
{% if email.mail_server_type == 0 %}
|
{% if config.mail_server_type == 0 %}
|
||||||
<div class="col-xs-12 col-sm-12">
|
<div class="col-xs-12 col-sm-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{email.mail_server}}</div>
|
<div class="col-xs-6 col-sm-3">{{config.mail_server}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{email.mail_port}}</div>
|
<div class="col-xs-6 col-sm-3">{{config.mail_port}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(email.mail_use_ssl) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.mail_use_ssl) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{email.mail_login}}</div>
|
<div class="col-xs-6 col-sm-3">{{config.mail_login}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('From Email')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{email.mail_from}}</div>
|
<div class="col-xs-6 col-sm-3">{{config.mail_from}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-xs-12 col-sm-12">
|
<div class="col-xs-12 col-sm-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('E-Mail Service')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Email Service')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Gmail via Oauth2')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Gmail via Oauth2')}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('From Email')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{email.mail_gmail_token['email']}}</div>
|
<div class="col-xs-6 col-sm-3">{{config.mail_gmail_token['email']}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-default emailconfig" id="admin_edit_email" href="{{url_for('admin.edit_mailsettings')}}">{{_('Edit E-mail Server Settings')}}</a>
|
<a class="btn btn-default emailconfig" id="admin_edit_email" href="{{url_for('admin.edit_mailsettings')}}">{{_('Edit Email Server Settings')}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -167,15 +167,15 @@
|
|||||||
<h2>{{_('Scheduled Tasks')}}</h2>
|
<h2>{{_('Scheduled Tasks')}}</h2>
|
||||||
<div class="col-xs-12 col-sm-12 scheduled_tasks_details">
|
<div class="col-xs-12 col-sm-12 scheduled_tasks_details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks start to run')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Start Time')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{schedule_time}}</div>
|
<div class="col-xs-6 col-sm-3">{{schedule_time}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Maximum tasks duration')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Maximum Duration')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{schedule_duration}}</div>
|
<div class="col-xs-6 col-sm-3">{{schedule_duration}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Generate book cover thumbnails')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Generate Thumbnails')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_book_covers) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_book_covers) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!--div class="row">
|
<!--div class="row">
|
||||||
@ -183,14 +183,18 @@
|
|||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_series_covers) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_series_covers) }}</div>
|
||||||
</div-->
|
</div-->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Reconnect to Calibre Library')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Reconnect Calibre Database')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_reconnect) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_reconnect) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6 col-sm-3">{{_('Generate Metadata Backup Files')}}</div>
|
||||||
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_metadata_backup) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-default scheduledtasks" id="admin_edit_scheduled_tasks" href="{{url_for('admin.edit_scheduledtasks')}}">{{_('Edit Scheduled Tasks Settings')}}</a>
|
<a class="btn btn-default scheduledtasks" id="admin_edit_scheduled_tasks" href="{{url_for('admin.edit_scheduledtasks')}}">{{_('Edit Scheduled Tasks Settings')}}</a>
|
||||||
{% if config.schedule_generate_book_covers %}
|
{% if config.schedule_generate_book_covers %}
|
||||||
<a class="btn btn-default" id="admin_refresh_cover_cache">{{_('Refresh Thumbnail Cover Cache')}}</a>
|
<a class="btn btn-default" id="admin_refresh_cover_cache">{{_('Refresh Thumbnail Cache')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -207,10 +211,11 @@
|
|||||||
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
|
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
|
||||||
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
|
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if config.schedule_metadata_backup %}
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<div class="btn btn-default" id="metadata_backup" data-toggle="modal" data-target="#StatusDialog">{{_('Queue all books for metadata backup')}}</div>
|
<div class="btn btn-default" id="metadata_backup" data-toggle="modal" data-target="#StatusDialog">{{_('Queue all books for metadata backup')}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>{{_('Version Information')}}</h2>
|
<h2>{{_('Version Information')}}</h2>
|
||||||
@ -224,7 +229,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr id="current_version">
|
<tr id="current_version">
|
||||||
<td>{{commit}} </td>
|
<td>{{commit}} </td>
|
||||||
<td><i>{{_('Current version')}}</i></td>
|
<td><i>{{_('Current Version')}}</i></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user