Merge branch 'master' into Develop
This commit is contained in:
commit
bc6a50550e
155
README.md
155
README.md
|
@ -1,109 +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, indonesian, italian, japanese, khmer, korean, norwegian, 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`
|
||||||
|
|
||||||
Issues with Raspberry Pi - Raspberry Pi OS:
|
*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.*
|
||||||
Depending on your version of pip it's possible that the installation fails with `Failed to build cryptography
|
|
||||||
ERROR: Could not build wheels for cryptography, which is required to install pyproject.toml-based projects`.
|
|
||||||
In this case please try to update pip with `./venv/bin/python3 -m pip install --upgrade pip` first, and then try installing Calibre-Web again.
|
|
||||||
If this isn't working please also install cargo via `sudo apt install cargo`, and try installing Calibre-Web again.
|
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
## Quick start
|
## Quick Start
|
||||||
|
|
||||||
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \
|
1. Open your browser and navigate to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||||
Login with default admin login \
|
2. Log in with the default admin credentials
|
||||||
If you don't have a Calibre database already, this [database](https://github.com/janeczku/calibre-web/blob/master/library/metadata.db) can be used. **IMPORTATNT** Please move the database out of the calibre-web folder structure, as it will be overwritten during update. \
|
3. If you don't have a Calibre database, you can use [this database](https://github.com/janeczku/calibre-web/blob/master/library/metadata.db) (move it out of the Calibre-Web folder to prevent overwriting during updates)
|
||||||
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button. \
|
4. Set `Location of Calibre database` to the path of the folder containing your Calibre library (metadata.db) and click "Save"
|
||||||
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) \
|
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)
|
||||||
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)
|
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
|
|
||||||
|
|
||||||
|
#### 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)
|
||||||
[Download](https://imagemagick.org/script/download.php) Imagemagick to extract covers from epubs. On Windows the additional installation of [ghostscript](https://ghostscript.com/releases/gsdnld.html) might be necessary to extract covers from pdf files. On Linux Imagemagick and Ghostscript can often be installed using the system package manager.
|
- 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)
|
||||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-ereader feature, or during editing of ebooks metadata:
|
|
||||||
|
|
||||||
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
|
|
||||||
|
|
||||||
[Download](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 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.
|
|
||||||
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
|
||||||
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
|
||||||
|
|
||||||
# Contact
|
Both the Calibre-Web and Calibre-Mod images are automatically rebuilt on new releases and updates.
|
||||||
|
|
||||||
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
|
- Set "path to convertertool" to `/usr/bin/ebook-convert`
|
||||||
|
- Set "path to unrar" to `/usr/bin/unrar`
|
||||||
|
|
||||||
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
## Contributor Recognition
|
||||||
|
|
||||||
# Contributing to Calibre-Web
|
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.
|
||||||
|
|
||||||
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
|
## Contact
|
||||||
|
|
||||||
|
Join us on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -28,10 +28,10 @@ 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
|
||||||
|
|
||||||
|
|
14
cps/admin.py
14
cps/admin.py
|
@ -30,6 +30,7 @@ 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
|
from flask_login import login_required, current_user, logout_user
|
||||||
|
@ -100,10 +101,12 @@ def admin_required(f):
|
||||||
|
|
||||||
@admi.before_app_request
|
@admi.before_app_request
|
||||||
def before_request():
|
def before_request():
|
||||||
if not ub.check_user_session(current_user.id, 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()
|
||||||
g.constants = constants
|
g.constants = constants
|
||||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
|
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
|
||||||
|
@ -1157,7 +1160,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")
|
||||||
|
@ -1172,6 +1174,11 @@ def _configuration_ldap_helper(to_save):
|
||||||
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
|
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
|
||||||
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")
|
||||||
|
|
||||||
|
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", "") != "":
|
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_e")
|
config.set_from_dictionary(to_save, "config_ldap_serv_password_e")
|
||||||
|
@ -1358,6 +1365,7 @@ def update_scheduledtasks():
|
||||||
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:
|
||||||
|
|
|
@ -153,6 +153,7 @@ 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_policy = Column(Boolean, default=True)
|
||||||
config_password_min_length = Column(Integer, default=8)
|
config_password_min_length = Column(Integer, default=8)
|
||||||
|
@ -404,9 +405,9 @@ def _encrypt_fields(session, secret_key):
|
||||||
session.query(exists().where(_Settings.mail_password_e)).scalar()
|
session.query(exists().where(_Settings.mail_password_e)).scalar()
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
with session.bind.connect() as conn:
|
with session.bind.connect() as conn:
|
||||||
conn.execute("ALTER TABLE settings ADD column 'mail_password_e' String")
|
conn.execute(text("ALTER TABLE settings ADD column 'mail_password_e' String"))
|
||||||
conn.execute("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String")
|
conn.execute(text("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String"))
|
||||||
conn.execute("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String")
|
conn.execute(text("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String"))
|
||||||
session.commit()
|
session.commit()
|
||||||
crypter = Fernet(secret_key)
|
crypter = Fernet(secret_key)
|
||||||
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
|
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
|
||||||
|
@ -530,7 +531,7 @@ def get_encryption_key(key_path):
|
||||||
key_file = os.path.join(key_path, ".key")
|
key_file = os.path.join(key_path, ".key")
|
||||||
generate = True
|
generate = True
|
||||||
error = ""
|
error = ""
|
||||||
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
|
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
|
||||||
with open(key_file, "rb") as f:
|
with open(key_file, "rb") as f:
|
||||||
key = f.read()
|
key = f.read()
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -147,7 +147,7 @@ EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
|
||||||
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr']
|
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr']
|
||||||
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', '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 +163,7 @@ def selected_roles(dictionary):
|
||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages, publisher, pubdate, identifiers')
|
'series_id, languages, publisher, pubdate, identifiers')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.20 Beta'}
|
STABLE_VERSION = {'version': '0.6.21 Beta'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = dict()
|
NIGHTLY_VERSION = dict()
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|
|
@ -829,8 +829,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('&')
|
||||||
|
@ -995,7 +993,7 @@ class CalibreDB:
|
||||||
title = title[len(prep):] + ', ' + prep
|
title = title[len(prep):] + ', ' + prep
|
||||||
return title.strip()
|
return title.strip()
|
||||||
|
|
||||||
conn = conn or self.session.connection().connection.connection
|
conn = conn or self.session.connection().connection.driver_connection
|
||||||
try:
|
try:
|
||||||
conn.create_function("title_sort", 1, _title_sort)
|
conn.create_function("title_sort", 1, _title_sort)
|
||||||
except sqliteOperationalError:
|
except sqliteOperationalError:
|
||||||
|
|
|
@ -226,7 +226,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(_("Oops! 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)
|
||||||
|
@ -302,7 +302,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(_("Oops! 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')
|
||||||
|
|
||||||
|
|
||||||
|
@ -451,7 +452,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
|
||||||
|
|
||||||
|
@ -563,7 +564,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:
|
||||||
|
@ -1199,7 +1200,8 @@ 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(_("Oops! 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
|
||||||
|
|
|
@ -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(text("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
|
|
||||||
"gdrive_id, path FROM gdrive_ids;"))
|
|
||||||
session.commit()
|
|
||||||
session.execute(text('DROP TABLE %s' % 'gdrive_ids'))
|
|
||||||
session.execute(text('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):
|
||||||
|
@ -344,7 +326,7 @@ def getFileFromEbooksFolder(path, fileName):
|
||||||
|
|
||||||
|
|
||||||
def moveGdriveFileRemote(origin_file_id, new_title):
|
def moveGdriveFileRemote(origin_file_id, new_title):
|
||||||
origin_file_id['title']= new_title
|
origin_file_id['title'] = new_title
|
||||||
origin_file_id.Upload()
|
origin_file_id.Upload()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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']}], })
|
||||||
driveFile.SetContentFile(f)
|
if not string:
|
||||||
|
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" %
|
||||||
|
|
|
@ -172,10 +172,6 @@ def check_send_to_ereader(entry):
|
||||||
book_formats.append({'format': 'Epub',
|
book_formats.append({'format': 'Epub',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to eReader', 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 eReader', format='Mobi')})
|
|
||||||
if 'PDF' in formats:
|
if 'PDF' in formats:
|
||||||
book_formats.append({'format': 'Pdf',
|
book_formats.append({'format': 'Pdf',
|
||||||
'convert': 0,
|
'convert': 0,
|
||||||
|
@ -195,7 +191,7 @@ def check_send_to_ereader(entry):
|
||||||
# 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):
|
||||||
|
@ -205,8 +201,8 @@ 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 eReader 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 eReader 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 eReader 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"""
|
||||||
|
@ -214,7 +210,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_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, '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, 'azw3', book_format.lower(), user_id, ereader_mail)
|
return convert_book_format(book_id, calibrepath, 'azw3', book_format.lower(), user_id, ereader_mail)
|
||||||
|
|
78
cps/kobo.py
78
cps/kobo.py
|
@ -48,7 +48,7 @@ import requests
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from . import isoLanguages
|
from . import isoLanguages
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
|
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
|
||||||
|
@ -165,16 +165,16 @@ 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:
|
#if sqlalchemy_version2:
|
||||||
changed_entries = select(db.Books,
|
# changed_entries = select(db.Books,
|
||||||
ub.ArchivedBook.last_modified,
|
# ub.ArchivedBook.last_modified,
|
||||||
ub.BookShelf.date_added,
|
# ub.BookShelf.date_added,
|
||||||
ub.ArchivedBook.is_archived)
|
# 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,
|
||||||
ub.BookShelf.date_added,
|
ub.BookShelf.date_added,
|
||||||
ub.ArchivedBook.is_archived)
|
ub.ArchivedBook.is_archived)
|
||||||
changed_entries = (changed_entries
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||||
ub.ArchivedBook.user_id == current_user.id))
|
ub.ArchivedBook.user_id == current_user.id))
|
||||||
|
@ -191,12 +191,12 @@ def HandleSyncRequest():
|
||||||
.filter(ub.Shelf.kobo_sync)
|
.filter(ub.Shelf.kobo_sync)
|
||||||
.distinct())
|
.distinct())
|
||||||
else:
|
else:
|
||||||
if sqlalchemy_version2:
|
#if sqlalchemy_version2:
|
||||||
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
# 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,
|
||||||
ub.ArchivedBook.is_archived)
|
ub.ArchivedBook.is_archived)
|
||||||
changed_entries = (changed_entries
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||||
ub.ArchivedBook.user_id == current_user.id))
|
ub.ArchivedBook.user_id == current_user.id))
|
||||||
|
@ -208,10 +208,10 @@ 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:
|
#if sqlalchemy_version2:
|
||||||
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
# books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
||||||
else:
|
#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:
|
||||||
formats = [data.format for data in book.Books.data]
|
formats = [data.format for data in book.Books.data]
|
||||||
|
@ -229,7 +229,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)
|
||||||
|
@ -242,7 +242,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(
|
||||||
|
@ -254,27 +254,27 @@ 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:
|
'''if sqlalchemy_version2:
|
||||||
max_change = calibre_db.session.execute(changed_entries
|
max_change = calibre_db.session.execute(changed_entries
|
||||||
.filter(ub.ArchivedBook.is_archived)
|
.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()))\
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
||||||
.columns(db.Books).first()
|
.columns(db.Books).first()
|
||||||
else:
|
else:'''
|
||||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
max_change = changed_entries.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()
|
||||||
|
|
||||||
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
||||||
|
|
||||||
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:
|
'''if sqlalchemy_version2:
|
||||||
entries = calibre_db.session.execute(changed_entries).all()
|
entries = calibre_db.session.execute(changed_entries).all()
|
||||||
book_count = len(entries)
|
book_count = len(entries)
|
||||||
else:
|
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)
|
||||||
log.debug("Remaining books to Sync: {}".format(book_count))
|
log.debug("Remaining books to Sync: {}".format(book_count))
|
||||||
|
@ -716,20 +716,20 @@ 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:
|
'''if sqlalchemy_version2:
|
||||||
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
|
shelflist = ub.session.execute(select(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),
|
||||||
ub.Shelf.user_id == current_user.id,
|
ub.Shelf.user_id == current_user.id,
|
||||||
*extra_filters
|
*extra_filters
|
||||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
|
||||||
else:
|
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),
|
||||||
ub.Shelf.user_id == current_user.id,
|
ub.Shelf.user_id == current_user.id,
|
||||||
*extra_filters
|
*extra_filters
|
||||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
||||||
|
|
||||||
for shelf in shelflist:
|
for shelf in shelflist:
|
||||||
if not shelf_lib.check_shelf_view_permissions(shelf):
|
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||||
|
|
|
@ -31,8 +31,8 @@ def get_scheduled_tasks(reconnect=True):
|
||||||
if reconnect:
|
if reconnect:
|
||||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||||
|
|
||||||
# ToDo make configurable. Generate metadata.opf file for each changed book
|
# Generate metadata.opf file for each changed book
|
||||||
if True:
|
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
|
||||||
|
|
|
@ -35,13 +35,12 @@ search = Blueprint('search', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
@search.route("/search", methods=["POST"])
|
@search.route("/search", methods=["GET"])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def simple_search():
|
def simple_search():
|
||||||
term = dict(request.form).get("query")
|
term = request.args.get("query")
|
||||||
if term:
|
if term:
|
||||||
flask_session['query'] = json.dumps(term.strip())
|
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
|
||||||
return redirect(url_for('web.books_list', data="search", sort_param='stored', query="")) # term.strip()
|
|
||||||
else:
|
else:
|
||||||
return render_title_template('search.html',
|
return render_title_template('search.html',
|
||||||
searchterm="",
|
searchterm="",
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
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, __version__
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
|
13
cps/shelf.py
13
cps/shelf.py
|
@ -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):
|
||||||
if cur_shelf.is_public:
|
try:
|
||||||
return True
|
if cur_shelf.is_public:
|
||||||
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
return True
|
||||||
log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
|
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
||||||
return False
|
log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.error(e)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -314,9 +314,6 @@ $(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");
|
||||||
|
|
||||||
|
@ -360,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,
|
||||||
|
@ -447,6 +419,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")
|
||||||
|
@ -529,6 +503,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"
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,35 @@
|
||||||
|
|
||||||
/* 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'),
|
||||||
|
success: function (data) {
|
||||||
|
handleResponse(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$("#have_read_form").ajaxForm();
|
$("#have_read_form").ajaxForm();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -434,7 +425,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);
|
||||||
|
|
||||||
|
@ -538,6 +529,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
|
||||||
|
@ -650,7 +642,6 @@ $(function() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#user_submit").click(function() {
|
$("#user_submit").click(function() {
|
||||||
this.closest("form").submit();
|
this.closest("form").submit();
|
||||||
});
|
});
|
||||||
|
@ -682,7 +673,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 ) {
|
||||||
|
@ -709,17 +700,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();
|
||||||
|
@ -745,7 +735,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()
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,10 +17,9 @@
|
||||||
# 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, gdriveutils, logger
|
from cps import config, db, gdriveutils, logger
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
|
@ -102,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")
|
||||||
|
@ -230,14 +208,7 @@ 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:
|
|
||||||
doc.write(f, xml_declaration=True, encoding='utf-8', pretty_print=True)
|
|
||||||
except Exception as ex:
|
|
||||||
raise Exception('Writing Metadata failed with error: {} '.format(ex))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -138,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)
|
||||||
|
|
||||||
|
|
|
@ -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=_("Tasks"), page="tasks")
|
|
||||||
|
|
||||||
|
|
||||||
# helper function to apply localize status information in tasklist entries
|
# helper function to apply localize status information in tasklist entries
|
||||||
|
|
|
@ -186,6 +186,10 @@
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Reconnect Calibre Database')}}</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>
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -358,7 +358,7 @@
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a class="accordion-toggle" data-toggle="collapse" href="#collapsesix">
|
<a class="accordion-toggle" data-toggle="collapse" href="#collapsesix">
|
||||||
<span class="glyphicon glyphicon-plus"></span>
|
<span class="glyphicon glyphicon-plus"></span>
|
||||||
{{_('Securitiy Settings')}}
|
{{_('Security Settings')}}
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
659
cps/templates/detail.html
Normal file → Executable file
659
cps/templates/detail.html
Normal file → Executable file
|
@ -1,326 +1,369 @@
|
||||||
{% extends is_xhr|yesno("fragment.html", "layout.html") %}
|
{% extends is_xhr|yesno("fragment.html", "layout.html") %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="single">
|
<div class="single">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-lg-3 col-xs-5">
|
<div class="col-sm-3 col-lg-3 col-xs-5">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<!-- Always use full-sized image for the detail page -->
|
<!-- Always use full-sized image for the detail page -->
|
||||||
<img id="detailcover" title="{{entry.title}}" src="{{url_for('web.get_cover', book_id=entry.id, resolution='og', c=entry|last_modified)}}" />
|
<img id="detailcover" title="{{ entry.title }}"
|
||||||
</div>
|
src="{{ url_for('web.get_cover', book_id=entry.id, resolution='og', c=entry|last_modified) }}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-9 col-lg-9 book-meta">
|
</div>
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="col-sm-9 col-lg-9 book-meta">
|
||||||
<div class="btn-group" role="group" aria-label="Download, send to eReader, reading">
|
<div class="btn-toolbar" role="toolbar">
|
||||||
{% if current_user.role_download() %}
|
<div class="btn-group" role="group" aria-label="Download, send to eReader, reading">
|
||||||
{% if entry.data|length %}
|
{% if current_user.role_download() %}
|
||||||
<div class="btn-group" role="group">
|
{% if entry.data|length %}
|
||||||
{% if entry.data|length < 2 %}
|
<div class="btn-group" role="group">
|
||||||
<button id="Download" type="button" class="btn btn-primary">
|
{% if entry.data|length < 2 %}
|
||||||
{{_('Download')}} :
|
<button id="Download" type="button" class="btn btn-primary">
|
||||||
</button>
|
{{ _('Download') }} :
|
||||||
{% for format in entry.data %}
|
</button>
|
||||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop1{{format.format|lower}}" class="btn btn-primary" role="button">
|
{% for format in entry.data %}
|
||||||
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}"
|
||||||
</a>
|
id="btnGroupDrop1{{ format.format|lower }}" class="btn btn-primary"
|
||||||
{% endfor %}
|
role="button">
|
||||||
{% else %}
|
<span class="glyphicon glyphicon-download"></span>{{ format.format }}
|
||||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
({{ format.uncompressed_size|filesizeformat }})
|
||||||
<span class="glyphicon glyphicon-download"></span> {{_('Download')}}
|
</a>
|
||||||
<span class="caret"></span>
|
{% endfor %}
|
||||||
</button>
|
{% else %}
|
||||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle"
|
||||||
{% for format in entry.data %}
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<li><a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{format.format}} ({{ format.uncompressed_size|filesizeformat }})</a></li>
|
<span class="glyphicon glyphicon-download"></span> {{ _('Download') }}
|
||||||
{% endfor %}
|
<span class="caret"></span>
|
||||||
</ul>
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||||
|
{% for format in entry.data %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{ format.format }}
|
||||||
|
({{ format.uncompressed_size|filesizeformat }})</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.kindle_mail and entry.email_share_list %}
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
{% if entry.email_share_list.__len__() == 1 %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button id="sendbtn" class="btn btn-primary sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}">
|
||||||
|
<span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="glyphicon glyphicon-send"></span>{{ _('Send to eReader') }}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="send-to-ereader">
|
||||||
|
{% for format in entry.email_share_list %}
|
||||||
|
<li>
|
||||||
|
<a class="sendbtn-form" data-href="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{ format['text'] }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.reader_list and current_user.role_viewer() %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if entry.reader_list|length > 1 %}
|
||||||
|
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="glyphicon glyphicon-book"></span> {{ _('Read in Browser') }}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
|
||||||
|
{% for format in entry.reader_list %}
|
||||||
|
<li><a target="_blank"
|
||||||
|
href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{ format }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<a target="_blank"
|
||||||
|
href="{{ url_for('web.read_book', book_id=entry.id, book_format=entry.reader_list[0]) }}"
|
||||||
|
id="readbtn" class="btn btn-primary" role="button"><span
|
||||||
|
class="glyphicon glyphicon-book"></span> {{ _('Read in Browser') }}
|
||||||
|
- {{ entry.reader_list[0] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.audio_entries|length > 0 and current_user.role_viewer() %}
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if entry.audio_entries|length > 1 %}
|
||||||
|
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="glyphicon glyphicon-music"></span> {{ _('Listen in Browser') }}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
|
||||||
|
{% for format in entry.reader_list %}
|
||||||
|
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{ format }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
|
||||||
|
|
||||||
|
{% for format in entry.data %}
|
||||||
|
{% if format.format|lower in entry.audio_entries %}
|
||||||
|
<li><a target="_blank"
|
||||||
|
href="{{ url_for('web.read_book', book_id=entry.id, book_format=format.format|lower) }}">{{ format.format|lower }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=entry.audio_entries[0]) }}" id="listenbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-music"></span> {{ _('Listen in Browser') }} - {{ entry.audio_entries[0] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 id="title">{{ entry.title }}</h2>
|
||||||
|
<p class="author">
|
||||||
|
{% for author in entry.ordered_authors %}
|
||||||
|
<a href="{{ url_for('web.books_list', data='author', sort_param='stored', book_id=author.id ) }}">{{ author.name.replace('|',',') }}</a>
|
||||||
|
{% if not loop.last %}
|
||||||
|
&
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% if entry.ratings.__len__() > 0 %}
|
||||||
|
<div class="rating">
|
||||||
|
<p>
|
||||||
|
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
|
||||||
|
<span class="glyphicon glyphicon-star good"></span>
|
||||||
|
{% if loop.last and loop.index < 5 %}
|
||||||
|
{% for numer in range(5 - loop.index) %}
|
||||||
|
<span class="glyphicon glyphicon-star-empty"></span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.series|length > 0 %}
|
||||||
|
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)|escapedlink(entry.series[0].name))|safe) }}</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.languages|length > 0 %}
|
||||||
|
<div class="languages">
|
||||||
|
<p>
|
||||||
|
<span class="label label-default">{{_('Language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.identifiers|length > 0 %}
|
||||||
|
<div class="identifiers">
|
||||||
|
<p>
|
||||||
|
<span class="glyphicon glyphicon-link"></span>
|
||||||
|
{% for identifier in entry.identifiers %}
|
||||||
|
<a href="{{ identifier }}" target="_blank" class="btn btn-xs btn-success"
|
||||||
|
role="button">{{ identifier.format_type() }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.tags|length > 0 %}
|
||||||
|
|
||||||
|
<div class="tags">
|
||||||
|
<p>
|
||||||
|
<span class="glyphicon glyphicon-tags"></span>
|
||||||
|
|
||||||
|
{% for tag in entry.tags %}
|
||||||
|
<a href="{{ url_for('web.books_list', data='category', sort_param='stored', book_id=tag.id) }}"
|
||||||
|
class="btn btn-xs btn-info" role="button">{{ tag.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.publishers|length > 0 %}
|
||||||
|
<div class="publishers">
|
||||||
|
<p>
|
||||||
|
<span>{{ _('Publisher') }}:
|
||||||
|
<a href="{{ url_for('web.books_list', data='publisher', sort_param='stored', book_id=entry.publishers[0].id ) }}">{{ entry.publishers[0].name }}</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if (entry.pubdate|string)[:10] != '0101-01-01' %}
|
||||||
|
<div class="publishing-date">
|
||||||
|
<p>{{ _('Published') }}: {{ entry.pubdate|formatdate }} </p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if cc|length > 0 %}
|
||||||
|
|
||||||
|
|
||||||
|
{% for c in cc %}
|
||||||
|
<div class="real_custom_columns">
|
||||||
|
{% if entry['custom_column_' ~ c.id]|length > 0 %}
|
||||||
|
{{ c.name }}:
|
||||||
|
{% for column in entry['custom_column_' ~ c.id] %}
|
||||||
|
{% if c.datatype == 'rating' %}
|
||||||
|
{{ (column.value / 2)|formatfloat }}
|
||||||
|
{% else %}
|
||||||
|
{% if c.datatype == 'bool' %}
|
||||||
|
{% if column.value == true %}
|
||||||
|
<span class="glyphicon glyphicon-ok"></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="glyphicon glyphicon-remove"></span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if c.datatype == 'float' %}
|
||||||
|
{{ column.value|formatfloat(2) }}
|
||||||
|
{% elif c.datatype == 'datetime' %}
|
||||||
|
{{ column.value|formatdate }}
|
||||||
|
{% elif c.datatype == 'comments' %}
|
||||||
|
{{ column.value|safe }}
|
||||||
|
{% elif c.datatype == 'series' %}
|
||||||
|
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||||
|
{% elif c.datatype == 'text' %}
|
||||||
|
{{ column.value.strip() }}{% if not loop.last %}, {% endif %}
|
||||||
|
{% else %}
|
||||||
|
{{ column.value }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if not current_user.is_anonymous %}
|
||||||
|
|
||||||
|
<div class="custom_columns">
|
||||||
|
<p>
|
||||||
|
<form id="have_read_form" action="{{ url_for('web.toggle_read', book_id=entry.id) }}"
|
||||||
|
method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<label class="block-label">
|
||||||
|
<input id="have_read_cb" data-checked="{{ _('Mark As Unread') }}"
|
||||||
|
data-unchecked="{{ _('Mark As Read') }}" type="checkbox"
|
||||||
|
{% if entry.read_status %}checked{% endif %}>
|
||||||
|
<span>{{ _('Read') }}</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
{% if current_user.check_visibility(32768) %}
|
||||||
|
<p>
|
||||||
|
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id) }}"
|
||||||
|
method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<label class="block-label">
|
||||||
|
<input id="archived_cb" data-checked="{{ _('Restore from archive') }}"
|
||||||
|
data-unchecked="{{ _('Add to archive') }}" type="checkbox"
|
||||||
|
{% if entry.is_archived %}checked{% endif %}>
|
||||||
|
<span>{{ _('Archived') }}</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||||
|
<div class="comments">
|
||||||
|
<h3 id="decription">{{ _('Description:') }}</h3>
|
||||||
|
{{ entry.comments[0].text|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="more-stuff">
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{% if current_user.shelf.all() or g.shelves_access %}
|
||||||
|
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||||
|
<div class="btn-group" role="group" aria-label="Add to shelves">
|
||||||
|
<button id="add-to-shelf" type="button"
|
||||||
|
class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"
|
||||||
|
aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="glyphicon glyphicon-list"></span> {{ _('Add to shelf') }}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
|
||||||
|
{% for shelf in g.shelves_access %}
|
||||||
|
{% if not shelf.id in books_shelfs and ( not shelf.is_public or current_user.role_edit_shelfs() ) %}
|
||||||
|
<li>
|
||||||
|
<a data-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
||||||
|
data-remove-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
||||||
|
data-shelf-action="add"
|
||||||
|
>
|
||||||
|
{{ shelf.name }}{% if shelf.is_public == 1 %}
|
||||||
|
{{ _('(Public)') }}{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="remove-from-shelves" class="btn-group" role="group"
|
||||||
|
aria-label="Remove from shelves">
|
||||||
|
{% if books_shelfs %}
|
||||||
|
{% for shelf in g.shelves_access %}
|
||||||
|
{% if shelf.id in books_shelfs %}
|
||||||
|
<a data-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
||||||
|
data-add-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
||||||
|
class="btn btn-sm btn-default" role="button"
|
||||||
|
data-shelf-action="remove"
|
||||||
|
>
|
||||||
|
<span {% if not shelf.is_public or current_user.role_edit_shelfs() %}
|
||||||
|
class="glyphicon glyphicon-remove"
|
||||||
|
{% endif %}></span> {{ shelf.name }}{% if shelf.is_public == 1 %} {{ _('(Public)') }}{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="shelf-action-errors" class="pull-left" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.role_edit() %}
|
||||||
|
<div class="btn-toolbar" role="toolbar">
|
||||||
|
<div class="btn-group" role="group" aria-label="Edit/Delete book">
|
||||||
|
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}"
|
||||||
|
class="btn btn-sm btn-primary" id="edit_book" role="button"><span
|
||||||
|
class="glyphicon glyphicon-edit"></span> {{ _('Edit Metadata') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if current_user.kindle_mail and entry.email_share_list %}
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
{% if entry.email_share_list.__len__() == 1 %}
|
|
||||||
<div id="sendbtn" data-action="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=entry.email_share_list[0]['format'], convert=entry.email_share_list[0]['convert'])}}" data-text="{{_('Send to eReader')}}" class="btn btn-primary postAction" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.email_share_list[0]['text']}}</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="glyphicon glyphicon-send"></span>{{_('Send to eReader')}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="send-to-ereader">
|
|
||||||
{% for format in entry.email_share_list %}
|
|
||||||
<li><a class="postAction" data-action="{{url_for('web.send_to_ereader', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
|
|
||||||
{%endfor%}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.reader_list and current_user.role_viewer() %}
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
{% if entry.reader_list|length > 1 %}
|
|
||||||
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="glyphicon glyphicon-book"></span> {{_('Read in Browser')}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
|
|
||||||
{% for format in entry.reader_list %}
|
|
||||||
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
|
|
||||||
{%endfor%}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=entry.reader_list[0])}}" id="readbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-book"></span> {{_('Read in Browser')}} - {{entry.reader_list[0]}}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.audio_entries|length > 0 and current_user.role_viewer() %}
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
{% if entry.audio_entries|length > 1 %}
|
|
||||||
<button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
|
|
||||||
{% for format in entry.reader_list %}
|
|
||||||
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
|
|
||||||
{%endfor%}
|
|
||||||
</ul>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="listen-in-browser">
|
|
||||||
|
|
||||||
{% for format in entry.data %}
|
|
||||||
{% if format.format|lower in entry.audio_entries %}
|
|
||||||
<li><a target="_blank" href="{{ url_for('web.read_book', book_id=entry.id, book_format=format.format|lower) }}">{{format.format|lower }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<a target="_blank" href="{{url_for('web.read_book', book_id=entry.id, book_format=entry.audio_entries[0])}}" id="listenbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-music"></span> {{_('Listen in Browser')}} - {{entry.audio_entries[0]}}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<h2 id="title">{{entry.title}}</h2>
|
|
||||||
<p class="author">
|
|
||||||
{% for author in entry.ordered_authors %}
|
|
||||||
<a href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
|
|
||||||
{% if not loop.last %}
|
|
||||||
&
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
{% if entry.ratings.__len__() > 0 %}
|
|
||||||
<div class="rating">
|
|
||||||
<p>
|
|
||||||
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
|
|
||||||
<span class="glyphicon glyphicon-star good"></span>
|
|
||||||
{% if loop.last and loop.index < 5 %}
|
|
||||||
{% for numer in range(5 - loop.index) %}
|
|
||||||
<span class="glyphicon glyphicon-star-empty"></span>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if entry.series|length > 0 %}
|
|
||||||
<p>{{_("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)|escapedlink(entry.series[0].name))|safe)}}</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.languages.__len__() > 0 %}
|
|
||||||
<div class="languages">
|
|
||||||
<p>
|
|
||||||
<span class="label label-default">{{_('Language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.identifiers|length > 0 %}
|
|
||||||
<div class="identifiers">
|
|
||||||
<p>
|
|
||||||
<span class="glyphicon glyphicon-link"></span>
|
|
||||||
{% for identifier in entry.identifiers %}
|
|
||||||
<a href="{{identifier}}" target="_blank" class="btn btn-xs btn-success" role="button">{{identifier.format_type()}}</a>
|
|
||||||
{%endfor%}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.tags|length > 0 %}
|
|
||||||
|
|
||||||
<div class="tags">
|
|
||||||
<p>
|
|
||||||
<span class="glyphicon glyphicon-tags"></span>
|
|
||||||
|
|
||||||
{% for tag in entry.tags %}
|
|
||||||
<a href="{{ url_for('web.books_list', data='category', sort_param='stored', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a>
|
|
||||||
{%endfor%}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.publishers|length > 0 %}
|
|
||||||
<div class="publishers">
|
|
||||||
<p>
|
|
||||||
<span>{{_('Publisher')}}:
|
|
||||||
<a href="{{url_for('web.books_list', data='publisher', sort_param='stored', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if (entry.pubdate|string)[:10] != '0101-01-01' %}
|
|
||||||
<div class="publishing-date">
|
|
||||||
<p>{{_('Published')}}: {{entry.pubdate|formatdate}} </p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if cc|length > 0 %}
|
|
||||||
|
|
||||||
|
|
||||||
{% for c in cc %}
|
|
||||||
<div class="real_custom_columns">
|
|
||||||
{% if entry['custom_column_' ~ c.id]|length > 0 %}
|
|
||||||
{{ c.name }}:
|
|
||||||
{% for column in entry['custom_column_' ~ c.id] %}
|
|
||||||
{% if c.datatype == 'rating' %}
|
|
||||||
{{ (column.value / 2)|formatfloat }}
|
|
||||||
{% else %}
|
|
||||||
{% if c.datatype == 'bool' %}
|
|
||||||
{% if column.value == true %}
|
|
||||||
<span class="glyphicon glyphicon-ok"></span>
|
|
||||||
{% else %}
|
|
||||||
<span class="glyphicon glyphicon-remove"></span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% if c.datatype == 'float' %}
|
|
||||||
{{ column.value|formatfloat(2) }}
|
|
||||||
{% elif c.datatype == 'datetime' %}
|
|
||||||
{{ column.value|formatdate }}
|
|
||||||
{% elif c.datatype == 'comments' %}
|
|
||||||
{{column.value|safe}}
|
|
||||||
{% elif c.datatype == 'series' %}
|
|
||||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
|
||||||
{% elif c.datatype == 'text' %}
|
|
||||||
{{ column.value.strip() }}{% if not loop.last %}, {% endif %}
|
|
||||||
{% else %}
|
|
||||||
{{ column.value }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if not current_user.is_anonymous %}
|
|
||||||
|
|
||||||
<div class="custom_columns">
|
|
||||||
<p>
|
|
||||||
<form id="have_read_form" action="{{ url_for('web.toggle_read', book_id=entry.id)}}" method="POST">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<label class="block-label">
|
|
||||||
<input id="have_read_cb" data-checked="{{_('Mark As Unread')}}" data-unchecked="{{_('Mark As Read')}}" type="checkbox" {% if entry.read_status %}checked{% endif %} >
|
|
||||||
<span>{{_('Read')}}</span>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</p>
|
|
||||||
{% if current_user.check_visibility(32768) %}
|
|
||||||
<p>
|
|
||||||
<form id="archived_form" action="{{ url_for('web.toggle_archived', book_id=entry.id)}}" method="POST">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<label class="block-label">
|
|
||||||
<input id="archived_cb" data-checked="{{_('Restore from archive')}}" data-unchecked="{{_('Add to archive')}}" type="checkbox" {% if entry.is_archived %}checked{% endif %} >
|
|
||||||
<span>{{_('Archived')}}</span>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
|
||||||
<div class="comments">
|
|
||||||
<h3 id="decription">{{_('Description:')}}</h3>
|
|
||||||
{{entry.comments[0].text|safe}}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="more-stuff">
|
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
{% if current_user.shelf.all() or g.shelves_access %}
|
|
||||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
|
||||||
<div class="btn-group" role="group" aria-label="Add to shelves">
|
|
||||||
<button id="add-to-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="glyphicon glyphicon-list"></span> {{_('Add to shelf')}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
|
|
||||||
{% for shelf in g.shelves_access %}
|
|
||||||
{% if not shelf.id in books_shelfs and ( not shelf.is_public or current_user.role_edit_shelfs() ) %}
|
|
||||||
<li>
|
|
||||||
<a data-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
|
||||||
data-remove-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
|
||||||
data-shelf-action="add"
|
|
||||||
>
|
|
||||||
{{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{%endfor%}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="remove-from-shelves" class="btn-group" role="group" aria-label="Remove from shelves">
|
|
||||||
{% if books_shelfs %}
|
|
||||||
{% for shelf in g.shelves_access %}
|
|
||||||
{% if shelf.id in books_shelfs %}
|
|
||||||
<a data-href="{{ url_for('shelf.remove_from_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
|
||||||
data-add-href="{{ url_for('shelf.add_to_shelf', book_id=entry.id, shelf_id=shelf.id) }}"
|
|
||||||
class="btn btn-sm btn-default" role="button" data-shelf-action="remove"
|
|
||||||
>
|
|
||||||
<span {% if not shelf.is_public or current_user.role_edit_shelfs() %}
|
|
||||||
class="glyphicon glyphicon-remove"
|
|
||||||
{% endif %}></span> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{%endfor%}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="shelf-action-errors" class="pull-left" role="alert"></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% if current_user.role_edit() %}
|
|
||||||
<div class="btn-toolbar" role="toolbar">
|
|
||||||
<div class="btn-group" role="group" aria-label="Edit/Delete book">
|
|
||||||
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/template" id="template-shelf-add">
|
<script type="text/template" id="template-shelf-add">
|
||||||
<li>
|
<li>
|
||||||
<a data-href="<%= add %>" data-remove-href="<%= remove %>" data-shelf-action="add">
|
<a data-href="<%= add %>" data-remove-href="<%= remove %>" data-shelf-action="add">
|
||||||
<%= content %>
|
<%= content %>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</script>
|
</script>
|
||||||
<script type="text/template" id="template-shelf-remove">
|
<script type="text/template" id="template-shelf-remove">
|
||||||
<a data-href="<%= remove %>" data-add-href="<%= add %>" class="btn btn-sm btn-default" data-shelf-action="remove">
|
<a data-href="<%= remove %>" data-add-href="<%= add %>" class="btn btn-sm btn-default"
|
||||||
<span class="glyphicon glyphicon-remove"></span> <%= content %>
|
data-shelf-action="remove">
|
||||||
</a>
|
<span class="glyphicon glyphicon-remove"></span> <%= content %>
|
||||||
|
</a>
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/details.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/details.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/fullscreen.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/fullscreen.js') }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,7 @@
|
||||||
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
|
<div class="plexBack"><a href="{{url_for('web.index')}}"></a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated or g.allow_anonymous %}
|
{% if current_user.is_authenticated or g.allow_anonymous %}
|
||||||
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="POST">
|
<form class="navbar-form navbar-left" role="search" action="{{url_for('search.simple_search')}}" method="GET">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<div class="form-group input-group input-group-sm">
|
<div class="form-group input-group input-group-sm">
|
||||||
<label for="query" class="sr-only">{{_('Search')}}</label>
|
<label for="query" class="sr-only">{{_('Search')}}</label>
|
||||||
<input type="text" class="form-control" id="query" name="query" placeholder="{{_('Search Library')}}" value="{{searchterm}}">
|
<input type="text" class="form-control" id="query" name="query" placeholder="{{_('Search Library')}}" value="{{searchterm}}">
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].id )}}{% endif %}">
|
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].id )}}{% endif %}">
|
||||||
{% if entry.name %}
|
{% if entry.name %}
|
||||||
<div class="rating">
|
<div class="rating">
|
||||||
{% for number in range(entry.name) %}
|
{% for number in range(entry.name|int) %}
|
||||||
<span class="glyphicon glyphicon-star good"></span>
|
<span class="glyphicon glyphicon-star good"></span>
|
||||||
{% if loop.last and loop.index < 5 %}
|
{% if loop.last and loop.index < 5 %}
|
||||||
{% for numer in range(5 - loop.index) %}
|
{% for numer in range(5 - loop.index) %}
|
||||||
|
|
|
@ -18,6 +18,6 @@
|
||||||
<script src="{{ url_for('static', filename='js/reading/djvu_reader.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/reading/djvu_reader.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="djvuContainer" file="{{ url_for('web.serve_book', book_id=djvufile, book_format='djvu') }}"></div>
|
<div id="djvuContainer" file="{{ url_for('web.serve_book', book_id=djvufile, book_format=extension) }}"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,10 @@
|
||||||
<input type="checkbox" id="schedule_reconnect" name="schedule_reconnect" {% if config.schedule_reconnect %}checked{% endif %}>
|
<input type="checkbox" id="schedule_reconnect" name="schedule_reconnect" {% if config.schedule_reconnect %}checked{% endif %}>
|
||||||
<label for="schedule_reconnect">{{_('Reconnect Calibre Database')}}</label>
|
<label for="schedule_reconnect">{{_('Reconnect Calibre Database')}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="schedule_metadata_backup" name="schedule_metadata_backup" {% if config.schedule_metadata_backup %}checked{% endif %}>
|
||||||
|
<label for="schedule_metadata_backup">{{_('Generate Metadata Backup Files')}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
|
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||||
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Cancel')}}</a>
|
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Cancel')}}</a>
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
49
cps/ub.py
49
cps/ub.py
|
@ -555,8 +555,9 @@ def add_missing_tables(engine, _session):
|
||||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
if not engine.dialect.has_table(engine.connect(), "registration"):
|
||||||
Registration.__table__.create(bind=engine)
|
Registration.__table__.create(bind=engine)
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
||||||
_session.commit()
|
trans.commit()
|
||||||
|
|
||||||
|
|
||||||
# migrate all settings missing in registration table
|
# migrate all settings missing in registration table
|
||||||
|
@ -566,16 +567,18 @@ def migrate_registration_table(engine, _session):
|
||||||
_session.commit()
|
_session.commit()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE registration ADD column 'allow' INTEGER"))
|
conn.execute(text("ALTER TABLE registration ADD column 'allow' INTEGER"))
|
||||||
conn.execute(text("update registration set 'allow' = 1"))
|
conn.execute(text("update registration set 'allow' = 1"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
try:
|
try:
|
||||||
# Handle table exists, but no content
|
# Handle table exists, but no content
|
||||||
cnt = _session.query(Registration).count()
|
cnt = _session.query(Registration).count()
|
||||||
if not cnt:
|
if not cnt:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("insert into registration (domain, allow) values('%.%',1)"))
|
conn.execute(text("insert into registration (domain, allow) values('%.%',1)"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
except exc.OperationalError: # Database is not writeable
|
except exc.OperationalError: # Database is not writeable
|
||||||
print('Settings database is not writeable. Exiting...')
|
print('Settings database is not writeable. Exiting...')
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
@ -598,11 +601,13 @@ def migrate_shelfs(engine, _session):
|
||||||
_session.query(exists().where(Shelf.uuid)).scalar()
|
_session.query(exists().where(Shelf.uuid)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'uuid' STRING"))
|
conn.execute(text("ALTER TABLE shelf ADD column 'uuid' STRING"))
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'created' DATETIME"))
|
conn.execute(text("ALTER TABLE shelf ADD column 'created' DATETIME"))
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'last_modified' DATETIME"))
|
conn.execute(text("ALTER TABLE shelf ADD column 'last_modified' DATETIME"))
|
||||||
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME"))
|
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME"))
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
||||||
|
trans.commit()
|
||||||
for shelf in _session.query(Shelf).all():
|
for shelf in _session.query(Shelf).all():
|
||||||
shelf.uuid = str(uuid.uuid4())
|
shelf.uuid = str(uuid.uuid4())
|
||||||
shelf.created = datetime.datetime.now()
|
shelf.created = datetime.datetime.now()
|
||||||
|
@ -615,16 +620,16 @@ def migrate_shelfs(engine, _session):
|
||||||
_session.query(exists().where(Shelf.kobo_sync)).scalar()
|
_session.query(exists().where(Shelf.kobo_sync)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
conn.execute(text("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_session.query(exists().where(BookShelf.order)).scalar()
|
_session.query(exists().where(BookShelf.order)).scalar()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1"))
|
conn.execute(text("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
|
|
||||||
|
|
||||||
def migrate_readBook(engine, _session):
|
def migrate_readBook(engine, _session):
|
||||||
|
@ -632,12 +637,13 @@ def migrate_readBook(engine, _session):
|
||||||
_session.query(exists().where(ReadBook.read_status)).scalar()
|
_session.query(exists().where(ReadBook.read_status)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0"))
|
conn.execute(text("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0"))
|
||||||
conn.execute(text("UPDATE book_read_link SET 'read_status' = 1 WHERE is_read"))
|
conn.execute(text("UPDATE book_read_link SET 'read_status' = 1 WHERE is_read"))
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME"))
|
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME"))
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME"))
|
conn.execute(text("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME"))
|
||||||
conn.execute(text("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0"))
|
conn.execute(text("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
test = _session.query(ReadBook).filter(ReadBook.last_modified == None).all()
|
test = _session.query(ReadBook).filter(ReadBook.last_modified == None).all()
|
||||||
for book in test:
|
for book in test:
|
||||||
book.last_modified = datetime.datetime.utcnow()
|
book.last_modified = datetime.datetime.utcnow()
|
||||||
|
@ -650,9 +656,10 @@ def migrate_remoteAuthToken(engine, _session):
|
||||||
_session.commit()
|
_session.commit()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0"))
|
conn.execute(text("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0"))
|
||||||
conn.execute(text("update remote_auth_token set 'token_type' = 0"))
|
conn.execute(text("update remote_auth_token set 'token_type' = 0"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
|
|
||||||
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
||||||
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
|
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
|
||||||
|
@ -669,16 +676,19 @@ def migrate_Database(_session):
|
||||||
_session.query(exists().where(User.sidebar_view)).scalar()
|
_session.query(exists().where(User.sidebar_view)).scalar()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1"))
|
conn.execute(text("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
create = True
|
create = True
|
||||||
try:
|
try:
|
||||||
if create:
|
if create:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("SELECT language_books FROM user"))
|
conn.execute(text("SELECT language_books FROM user"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
|
conn.execute(text("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
|
||||||
"+ series_books * :side_series + category_books * :side_category + hot_books * "
|
"+ series_books * :side_series + category_books * :side_category + hot_books * "
|
||||||
":side_hot + :side_autor + :detail_random)"),
|
":side_hot + :side_autor + :detail_random)"),
|
||||||
|
@ -686,35 +696,38 @@ def migrate_Database(_session):
|
||||||
'side_series': constants.SIDEBAR_SERIES, 'side_category': constants.SIDEBAR_CATEGORY,
|
'side_series': constants.SIDEBAR_SERIES, 'side_category': constants.SIDEBAR_CATEGORY,
|
||||||
'side_hot': constants.SIDEBAR_HOT, 'side_autor': constants.SIDEBAR_AUTHOR,
|
'side_hot': constants.SIDEBAR_HOT, 'side_autor': constants.SIDEBAR_AUTHOR,
|
||||||
'detail_random': constants.DETAIL_RANDOM})
|
'detail_random': constants.DETAIL_RANDOM})
|
||||||
_session.commit()
|
trans.commit()
|
||||||
try:
|
try:
|
||||||
_session.query(exists().where(User.denied_tags)).scalar()
|
_session.query(exists().where(User.denied_tags)).scalar()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''"))
|
conn.execute(text("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''"))
|
conn.execute(text("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE user ADD column `denied_column_value` String DEFAULT ''"))
|
conn.execute(text("ALTER TABLE user ADD column `denied_column_value` String DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''"))
|
conn.execute(text("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
try:
|
try:
|
||||||
_session.query(exists().where(User.view_settings)).scalar()
|
_session.query(exists().where(User.view_settings)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'"))
|
conn.execute(text("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
try:
|
try:
|
||||||
_session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
|
_session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
conn.execute("ALTER TABLE user ADD column `kobo_only_shelves_sync` SMALLINT DEFAULT 0")
|
trans = conn.begin()
|
||||||
_session.commit()
|
conn.execute(text("ALTER TABLE user ADD column `kobo_only_shelves_sync` SMALLINT DEFAULT 0"))
|
||||||
|
trans.commit()
|
||||||
try:
|
try:
|
||||||
# check if name is in User table instead of nickname
|
# check if name is in User table instead of nickname
|
||||||
_session.query(exists().where(User.name)).scalar()
|
_session.query(exists().where(User.name)).scalar()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
# Create new table user_id and copy contents of table user into it
|
# Create new table user_id and copy contents of table user into it
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
trans = conn.begin()
|
||||||
conn.execute(text("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
|
conn.execute(text("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
|
||||||
"name VARCHAR(64),"
|
"name VARCHAR(64),"
|
||||||
"email VARCHAR(120),"
|
"email VARCHAR(120),"
|
||||||
|
@ -741,7 +754,7 @@ def migrate_Database(_session):
|
||||||
# delete old user table and rename new user_id table to user:
|
# delete old user table and rename new user_id table to user:
|
||||||
conn.execute(text("DROP TABLE user"))
|
conn.execute(text("DROP TABLE user"))
|
||||||
conn.execute(text("ALTER TABLE user_id RENAME TO user"))
|
conn.execute(text("ALTER TABLE user_id RENAME TO user"))
|
||||||
_session.commit()
|
trans.commit()
|
||||||
if _session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
|
if _session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
|
||||||
is None:
|
is None:
|
||||||
create_anonymous_user(_session)
|
create_anonymous_user(_session)
|
||||||
|
|
27
cps/web.py
Normal file → Executable file
27
cps/web.py
Normal file → Executable file
|
@ -25,7 +25,7 @@ import chardet # dependency of requests
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
|
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for, Response
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
|
@ -396,7 +396,7 @@ def render_books_list(data, sort_param, book_id, page):
|
||||||
elif data == "archived":
|
elif data == "archived":
|
||||||
return render_archived_books(page, order)
|
return render_archived_books(page, order)
|
||||||
elif data == "search":
|
elif data == "search":
|
||||||
term = json.loads(flask_session.get('query', ''))
|
term = (request.args.get('query') or '')
|
||||||
offset = int(int(config.config_books_per_page) * (page - 1))
|
offset = int(int(config.config_books_per_page) * (page - 1))
|
||||||
return render_search_results(term, offset, order, config.config_books_per_page)
|
return render_search_results(term, offset, order, config.config_books_per_page)
|
||||||
elif data == "advsearch":
|
elif data == "advsearch":
|
||||||
|
@ -1214,22 +1214,20 @@ def download_link(book_id, book_format, anyname):
|
||||||
@download_required
|
@download_required
|
||||||
def send_to_ereader(book_id, book_format, convert):
|
def send_to_ereader(book_id, book_format, convert):
|
||||||
if not config.get_mail_server_configured():
|
if not config.get_mail_server_configured():
|
||||||
flash(_("Please configure the SMTP mail settings first."), category="error")
|
response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}]
|
||||||
|
return Response(json.dumps(response), mimetype='application/json')
|
||||||
elif current_user.kindle_mail:
|
elif current_user.kindle_mail:
|
||||||
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir,
|
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir,
|
||||||
current_user.name)
|
current_user.name)
|
||||||
if result is None:
|
if result is None:
|
||||||
flash(_("Success! Book queued for sending to %(eReadermail)s", eReadermail=current_user.kindle_mail),
|
|
||||||
category="success")
|
|
||||||
ub.update_download(book_id, int(current_user.id))
|
ub.update_download(book_id, int(current_user.id))
|
||||||
|
response = [{'type': "success", 'message': _("Success! Book queued for sending to %(eReadermail)s",
|
||||||
|
eReadermail=current_user.kindle_mail)}]
|
||||||
else:
|
else:
|
||||||
flash(_("Oops! There was an error sending book: %(res)s", res=result), category="error")
|
response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}]
|
||||||
else:
|
else:
|
||||||
flash(_("Oops! Please update your profile with a valid eReader Email."), category="error")
|
response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}]
|
||||||
if "HTTP_REFERER" in request.environ:
|
return Response(json.dumps(response), mimetype='application/json')
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
|
||||||
else:
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
|
|
||||||
# ################################### Login Logout ##################################################################
|
# ################################### Login Logout ##################################################################
|
||||||
|
@ -1518,7 +1516,6 @@ def profile():
|
||||||
@viewer_required
|
@viewer_required
|
||||||
def read_book(book_id, book_format):
|
def read_book(book_id, book_format):
|
||||||
book = calibre_db.get_filtered_book(book_id)
|
book = calibre_db.get_filtered_book(book_id)
|
||||||
book.ordered_authors = calibre_db.order_authors([book], False)
|
|
||||||
|
|
||||||
if not book:
|
if not book:
|
||||||
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
|
||||||
|
@ -1526,6 +1523,8 @@ def read_book(book_id, book_format):
|
||||||
log.debug("Selected book is unavailable. File does not exist or is not accessible")
|
log.debug("Selected book is unavailable. File does not exist or is not accessible")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
|
book.ordered_authors = calibre_db.order_authors([book], False)
|
||||||
|
|
||||||
# check if book has a bookmark
|
# check if book has a bookmark
|
||||||
bookmark = None
|
bookmark = None
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
|
@ -1541,9 +1540,9 @@ def read_book(book_id, book_format):
|
||||||
elif book_format.lower() == "txt":
|
elif book_format.lower() == "txt":
|
||||||
log.debug("Start txt reader for %d", book_id)
|
log.debug("Start txt reader for %d", book_id)
|
||||||
return render_title_template('readtxt.html', txtfile=book_id, title=book.title)
|
return render_title_template('readtxt.html', txtfile=book_id, title=book.title)
|
||||||
elif book_format.lower() == "djvu":
|
elif book_format.lower() in ["djvu", "djv"]:
|
||||||
log.debug("Start djvu reader for %d", book_id)
|
log.debug("Start djvu reader for %d", book_id)
|
||||||
return render_title_template('readdjvu.html', djvufile=book_id, title=book.title)
|
return render_title_template('readdjvu.html', djvufile=book_id, title=book.title, extension=book_format.lower())
|
||||||
else:
|
else:
|
||||||
for fileExt in constants.EXTENSIONS_AUDIO:
|
for fileExt in constants.EXTENSIONS_AUDIO:
|
||||||
if book_format.lower() == fileExt:
|
if book_format.lower() == fileExt:
|
||||||
|
|
862
messages.pot
862
messages.pot
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,8 @@
|
||||||
# GDrive Integration
|
# GDrive Integration
|
||||||
google-api-python-client>=1.7.11,<2.78.0
|
google-api-python-client>=1.7.11,<2.90.0
|
||||||
gevent>20.6.0,<23.0.0
|
gevent>20.6.0,<23.0.0
|
||||||
greenlet>=0.4.17,<2.1.0
|
greenlet>=0.4.17,<2.1.0
|
||||||
httplib2>=0.9.2,<0.22.0
|
httplib2>=0.9.2,<0.23.0
|
||||||
oauth2client>=4.0.0,<4.1.4
|
oauth2client>=4.0.0,<4.1.4
|
||||||
uritemplate>=3.0.0,<4.2.0
|
uritemplate>=3.0.0,<4.2.0
|
||||||
pyasn1-modules>=0.0.8,<0.3.0
|
pyasn1-modules>=0.0.8,<0.3.0
|
||||||
|
@ -13,7 +13,7 @@ rsa>=3.4.2,<4.10.0
|
||||||
|
|
||||||
# Gmail
|
# Gmail
|
||||||
google-auth-oauthlib>=0.4.3,<0.9.0
|
google-auth-oauthlib>=0.4.3,<0.9.0
|
||||||
google-api-python-client>=1.7.11,<2.78.0
|
google-api-python-client>=1.7.11,<2.90.0
|
||||||
|
|
||||||
# goodreads
|
# goodreads
|
||||||
goodreads>=0.3.2,<0.4.0
|
goodreads>=0.3.2,<0.4.0
|
||||||
|
@ -34,7 +34,7 @@ markdown2>=2.0.0,<2.5.0
|
||||||
html2text>=2020.1.16,<2022.1.1
|
html2text>=2020.1.16,<2022.1.1
|
||||||
python-dateutil>=2.1,<2.9.0
|
python-dateutil>=2.1,<2.9.0
|
||||||
beautifulsoup4>=4.0.1,<4.12.0
|
beautifulsoup4>=4.0.1,<4.12.0
|
||||||
cchardet>=2.0.0,<2.2.0
|
faust-cchardet>=2.1.18
|
||||||
|
|
||||||
# Comics
|
# Comics
|
||||||
natsort>=2.2.0,<8.4.0
|
natsort>=2.2.0,<8.4.0
|
||||||
|
|
|
@ -4,10 +4,9 @@ Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<3.1.0
|
Flask-Babel>=0.11.1,<3.1.0
|
||||||
Flask-Login>=0.3.2,<0.6.3
|
Flask-Login>=0.3.2,<0.6.3
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
backports_abc>=0.4
|
|
||||||
Flask>=1.0.2,<2.3.0
|
Flask>=1.0.2,<2.3.0
|
||||||
iso-639>=0.4.5,<0.5.0
|
iso-639>=0.4.5,<0.5.0
|
||||||
PyPDF>=3.0.0,<3.6.0
|
PyPDF>=3.0.0,<3.8.0
|
||||||
pytz>=2016.10
|
pytz>=2016.10
|
||||||
requests>=2.11.1,<2.29.0
|
requests>=2.11.1,<2.29.0
|
||||||
SQLAlchemy>=1.3.0,<2.0.0
|
SQLAlchemy>=1.3.0,<2.0.0
|
||||||
|
|
|
@ -63,10 +63,10 @@ install_requires =
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
gdrive =
|
gdrive =
|
||||||
google-api-python-client>=1.7.11,<2.78.0
|
google-api-python-client>=1.7.11,<2.90.0
|
||||||
gevent>20.6.0,<23.0.0
|
gevent>20.6.0,<23.0.0
|
||||||
greenlet>=0.4.17,<2.1.0
|
greenlet>=0.4.17,<2.1.0
|
||||||
httplib2>=0.9.2,<0.22.0
|
httplib2>=0.9.2,<0.23.0
|
||||||
oauth2client>=4.0.0,<4.1.4
|
oauth2client>=4.0.0,<4.1.4
|
||||||
uritemplate>=3.0.0,<4.2.0
|
uritemplate>=3.0.0,<4.2.0
|
||||||
pyasn1-modules>=0.0.8,<0.3.0
|
pyasn1-modules>=0.0.8,<0.3.0
|
||||||
|
@ -76,7 +76,7 @@ gdrive =
|
||||||
rsa>=3.4.2,<4.10.0
|
rsa>=3.4.2,<4.10.0
|
||||||
gmail =
|
gmail =
|
||||||
google-auth-oauthlib>=0.4.3,<0.9.0
|
google-auth-oauthlib>=0.4.3,<0.9.0
|
||||||
google-api-python-client>=1.7.11,<2.78.0
|
google-api-python-client>=1.7.11,<2.90.0
|
||||||
goodreads =
|
goodreads =
|
||||||
goodreads>=0.3.2,<0.4.0
|
goodreads>=0.3.2,<0.4.0
|
||||||
python-Levenshtein>=0.12.0,<0.21.0
|
python-Levenshtein>=0.12.0,<0.21.0
|
||||||
|
@ -93,7 +93,6 @@ metadata =
|
||||||
html2text>=2020.1.16,<2022.1.1
|
html2text>=2020.1.16,<2022.1.1
|
||||||
python-dateutil>=2.1,<2.9.0
|
python-dateutil>=2.1,<2.9.0
|
||||||
beautifulsoup4>=4.0.1,<4.12.0
|
beautifulsoup4>=4.0.1,<4.12.0
|
||||||
cchardet>=2.0.0,<2.2.0
|
|
||||||
comics =
|
comics =
|
||||||
natsort>=2.2.0,<8.4.0
|
natsort>=2.2.0,<8.4.0
|
||||||
comicapi>=2.2.0,<2.3.0
|
comicapi>=2.2.0,<2.3.0
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user