diff --git a/README.md b/README.md
index 5b137f68..0ac9a8d8 100755
--- a/README.md
+++ b/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)
-[![GitHub 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)
+[![License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
+![Commit Activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)
+[![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 - 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)
+
+Table of Contents (click to expand)
+
+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)
+
+
+
+
*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)
## Features
-- Bootstrap 3 HTML5 interface
-- full graphical setup
-- User management with fine-grained per-user permissions
+- Modern and responsive Bootstrap 3 HTML5 interface
+- Full graphical setup
+- Comprehensive user management with fine-grained per-user permissions
- 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
-- OPDS feed for eBook reader apps
-- Filter and search by titles, authors, tags, series, book format and language
-- Create a custom book collection (shelves)
-- Support for editing eBook metadata and deleting eBooks from Calibre library
-- Support for downloading eBook metadata from various sources, sources can be extended via external plugins
-- Support for converting eBooks through Calibre binaries
-- Restrict eBook download to logged-in users
-- Support for public user registration
-- Send eBooks to E-Readers with the click of a button
-- Sync your Kobo devices through Calibre-Web with your Calibre library
-- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
-- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
-- Support for Calibre Custom Columns
-- Ability to hide content based on categories and Custom Column content per user
+- Multilingual user interface supporting 20+ languages ([supported languages](https://github.com/janeczku/calibre-web/wiki/Translation-Status))
+- OPDS feed for eBook reader apps
+- Advanced search and filtering options
+- Custom book collection (shelves) creation
+- eBook metadata editing and deletion support
+- Metadata download from various sources (extensible via plugins)
+- eBook conversion through Calibre binaries
+- eBook download restriction to logged-in users
+- Public user registration support
+- Send eBooks to E-Readers with a single click
+- Sync Kobo devices with your Calibre library
+- In-browser eBook reading support for multiple formats
+- Upload new books in various formats, including audio formats
+- Calibre Custom Columns support
+- Content hiding based on categories and Custom Column content per user
- Self-update capability
-- "Magic Link" login to make it easy to log on eReaders
-- Login via LDAP, google/github oauth and via proxy authentication
+- "Magic Link" login for easy access on eReaders
+- LDAP, Google/GitHub OAuth, and proxy authentication support
## Installation
#### Installation via pip (recommended)
-1. To avoid problems with already installed python dependencies, it's recommended to create a virtual environment for Calibre-Web
-2. Install Calibre-Web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
-3. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details
-4. Calibre-Web can be started afterwards by typing `cps`
+1. Create a virtual environment for Calibre-Web to avoid conflicts with existing Python dependencies
+2. Install Calibre-Web via pip: `pip install calibreweb` (or `pip3` depending on your OS/distro)
+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. Start Calibre-Web by typing `cps`
-Issues with Raspberry Pi - Raspberry Pi OS:
-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.
+*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.*
-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 \
-Login with default admin login \
-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. \
-Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button. \
-Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration) \
-Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page)
-
-#### Default admin login:
-*Username:* admin\
-*Password:* admin123
+1. Open your browser and navigate to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
+2. Log in with the default admin credentials
+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)
+4. Set `Location of Calibre database` to the path of the folder containing your Calibre library (metadata.db) and click "Save"
+5. Optionally, use Google Drive to host your Calibre library by following the [Google Drive integration guide](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration)
+6. Configure your Calibre-Web instance via the admin page, referring to the [Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) guides
+#### Default Admin Login:
+- **Username:** admin
+- **Password:** admin123
## Requirements
-python 3.5+
-
-[Download](https://imagemagick.org/script/download.php) Imagemagick to extract covers from epubs. On Windows the additional installation of [ghostscript](https://ghostscript.com/releases/gsdnld.html) might be necessary to extract covers from pdf files. On Linux Imagemagick and Ghostscript can often be installed using the system package manager.
-
-Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-ereader feature, or during editing of ebooks metadata:
-
-[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`.
-
+- 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)
+- 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)
## 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**
-+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
-+ Github - [https://github.com/linuxserver/docker-calibre-web](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)
+#### **LinuxServer - x64, aarch64**
+- [Docker Hub](https://hub.docker.com/r/linuxserver/calibre-web)
+- [GitHub](https://github.com/linuxserver/docker-calibre-web)
+- [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)**
-
- 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`
+ 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.
-# 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)
diff --git a/cps/MyLoginManager.py b/cps/MyLoginManager.py
index 2587e1a9..39e7e4a5 100644
--- a/cps/MyLoginManager.py
+++ b/cps/MyLoginManager.py
@@ -28,10 +28,10 @@ from flask_login.signals import user_loaded_from_cookie
class MyLoginManager(LoginManager):
def _session_protection_failed(self):
- _session = session._get_current_object()
+ sess = session._get_current_object()
ident = self._session_identifier_generator()
- if(_session and not (len(_session) == 1
- and _session.get('csrf_token', None))) and ident != _session.get('_id', None):
+ if(sess and not (len(sess) == 1
+ and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
return super(). _session_protection_failed()
return False
diff --git a/cps/admin.py b/cps/admin.py
index 610afa17..82fc196e 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -30,6 +30,7 @@ import string
from datetime import datetime, timedelta
from datetime import time as datetime_time
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_login import login_required, current_user, logout_user
@@ -100,10 +101,12 @@ def admin_required(f):
@admi.before_app_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()
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_anonymous = config.config_anonbrowse
g.allow_upload = config.config_uploading
@@ -1157,7 +1160,6 @@ def _configuration_logfile_helper(to_save):
def _configuration_ldap_helper(to_save):
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_authentication")
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_key_path")
_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", "") != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password_e")
@@ -1358,6 +1365,7 @@ def update_scheduledtasks():
error = True
_config_checkbox(to_save, "schedule_generate_book_covers")
_config_checkbox(to_save, "schedule_generate_series_covers")
+ _config_checkbox(to_save, "schedule_metadata_backup")
_config_checkbox(to_save, "schedule_reconnect")
if not error:
diff --git a/cps/config_sql.py b/cps/config_sql.py
index 771b353c..91e4b6af 100644
--- a/cps/config_sql.py
+++ b/cps/config_sql.py
@@ -153,6 +153,7 @@ class _Settings(_Base):
schedule_generate_book_covers = Column(Boolean, default=False)
schedule_generate_series_covers = Column(Boolean, default=False)
schedule_reconnect = Column(Boolean, default=False)
+ schedule_metadata_backup = Column(Boolean, default=False)
config_password_policy = Column(Boolean, default=True)
config_password_min_length = Column(Integer, default=8)
@@ -404,9 +405,9 @@ def _encrypt_fields(session, secret_key):
session.query(exists().where(_Settings.mail_password_e)).scalar()
except OperationalError:
with session.bind.connect() as conn:
- conn.execute("ALTER TABLE settings ADD column 'mail_password_e' String")
- conn.execute("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 'mail_password_e' String"))
+ conn.execute(text("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String"))
+ conn.execute(text("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String"))
session.commit()
crypter = Fernet(secret_key)
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
@@ -530,7 +531,7 @@ def get_encryption_key(key_path):
key_file = os.path.join(key_path, ".key")
generate = True
error = ""
- if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
+ if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
with open(key_file, "rb") as f:
key = f.read()
try:
diff --git a/cps/constants.py b/cps/constants.py
index 376e1097..069630b6 100644
--- a/cps/constants.py
+++ b/cps/constants.py
@@ -147,7 +147,7 @@ EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr']
EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
'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',
'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, '
'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[0] = '$Format:%H$'
diff --git a/cps/db.py b/cps/db.py
index 8cc48e1d..81f46b81 100644
--- a/cps/db.py
+++ b/cps/db.py
@@ -829,8 +829,6 @@ class CalibreDB:
# Orders all Authors in the list according to authors sort
def order_authors(self, entries, list_return=False, combined=False):
- # entries_copy = copy.deepcopy(entries)
- # entries_copy =[]
for entry in entries:
if combined:
sort_authors = entry.Books.author_sort.split('&')
@@ -995,7 +993,7 @@ class CalibreDB:
title = title[len(prep):] + ', ' + prep
return title.strip()
- conn = conn or self.session.connection().connection.connection
+ conn = conn or self.session.connection().connection.driver_connection
try:
conn.create_function("title_sort", 1, _title_sort)
except sqliteOperationalError:
diff --git a/cps/editbooks.py b/cps/editbooks.py
index 06c8b12b..e87ea961 100755
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -226,7 +226,7 @@ def edit_book(book_id):
except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e:
log.error_or_exception("Database error: {}".format(e))
calibre_db.session.rollback()
- flash(_("Oops! Database Error: %(error)s.", error=e.orig), 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))
except Exception as ex:
log.error_or_exception(ex)
@@ -302,7 +302,8 @@ def upload():
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
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')
@@ -451,7 +452,7 @@ def edit_list_book(param):
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
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')
return ret
@@ -563,7 +564,7 @@ def table_xchange_author_title():
calibre_db.session.commit()
except (OperationalError, IntegrityError, StaleDataError) as e:
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})
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:
calibre_db.session.rollback()
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))
# Queue uploader info
diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py
index e2db770d..08ead47d 100644
--- a/cps/gdriveutils.py
+++ b/cps/gdriveutils.py
@@ -147,7 +147,7 @@ engine = create_engine('sqlite:///{0}'.format(cli_param.gd_path), echo=False)
Base = declarative_base()
# Open session for database connection
-Session = sessionmaker()
+Session = sessionmaker(autoflush=False)
Session.configure(bind=engine)
session = scoped_session(Session)
@@ -174,30 +174,12 @@ class PermissionAdded(Base):
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):
try:
Base.metadata.create_all(engine)
except Exception as ex:
log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
raise
-migrate()
def getDrive(drive=None, gauth=None):
@@ -344,7 +326,7 @@ def getFileFromEbooksFolder(path, fileName):
def moveGdriveFileRemote(origin_file_id, new_title):
- origin_file_id['title']= new_title
+ origin_file_id['title'] = new_title
origin_file_id.Upload()
@@ -422,7 +404,7 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
driveFile.Upload()
-def uploadFileToEbooksFolder(destFile, f):
+def uploadFileToEbooksFolder(destFile, f, string=False):
drive = getDrive(Gdrive.Instance().drive)
parent = getEbooksFolder(drive)
splitDir = destFile.split('/')
@@ -435,7 +417,10 @@ def uploadFileToEbooksFolder(destFile, f):
else:
driveFile = drive.CreateFile({'title': x,
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
- driveFile.SetContentFile(f)
+ if not string:
+ driveFile.SetContentFile(f)
+ else:
+ driveFile.SetContentString(f)
driveFile.Upload()
else:
existing_Folder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
diff --git a/cps/helper.py b/cps/helper.py
index 85d71122..be0323a2 100644
--- a/cps/helper.py
+++ b/cps/helper.py
@@ -172,10 +172,6 @@ def check_send_to_ereader(entry):
book_formats.append({'format': 'Epub',
'convert': 0,
'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:
book_formats.append({'format': 'Pdf',
'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
# list with supported formats
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()
if len(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:
-# 1: If Mobi file is existing, it's directly send to eReader email,
-# 2: If Epub file is existing, it's converted and send to eReader email,
+# 1: If epub file is existing, it's directly 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
def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id):
"""Send email with attachments"""
@@ -214,7 +210,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id)
if convert == 1:
# 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:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, 'azw3', book_format.lower(), user_id, ereader_mail)
diff --git a/cps/kobo.py b/cps/kobo.py
index de5d3235..a8cdf25c 100644
--- a/cps/kobo.py
+++ b/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 isoLanguages
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 .services import SyncToken as SyncToken
from .web import download_required
@@ -165,16 +165,16 @@ def HandleSyncRequest():
only_kobo_shelves = current_user.kobo_only_shelves_sync
if only_kobo_shelves:
- if sqlalchemy_version2:
- changed_entries = select(db.Books,
- ub.ArchivedBook.last_modified,
- ub.BookShelf.date_added,
- ub.ArchivedBook.is_archived)
- else:
- changed_entries = calibre_db.session.query(db.Books,
- ub.ArchivedBook.last_modified,
- ub.BookShelf.date_added,
- ub.ArchivedBook.is_archived)
+ #if sqlalchemy_version2:
+ # changed_entries = select(db.Books,
+ # ub.ArchivedBook.last_modified,
+ # ub.BookShelf.date_added,
+ # ub.ArchivedBook.is_archived)
+ #else:
+ changed_entries = calibre_db.session.query(db.Books,
+ ub.ArchivedBook.last_modified,
+ ub.BookShelf.date_added,
+ ub.ArchivedBook.is_archived)
changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
ub.ArchivedBook.user_id == current_user.id))
@@ -191,12 +191,12 @@ def HandleSyncRequest():
.filter(ub.Shelf.kobo_sync)
.distinct())
else:
- if sqlalchemy_version2:
- changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
- else:
- changed_entries = calibre_db.session.query(db.Books,
- ub.ArchivedBook.last_modified,
- ub.ArchivedBook.is_archived)
+ #if sqlalchemy_version2:
+ # changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
+ #else:
+ changed_entries = calibre_db.session.query(db.Books,
+ ub.ArchivedBook.last_modified,
+ ub.ArchivedBook.is_archived)
changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
ub.ArchivedBook.user_id == current_user.id))
@@ -208,10 +208,10 @@ def HandleSyncRequest():
.order_by(db.Books.id))
reading_states_in_new_entitlements = []
- if sqlalchemy_version2:
- books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
- else:
- books = changed_entries.limit(SYNC_ITEM_LIMIT)
+ #if sqlalchemy_version2:
+ # books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
+ #else:
+ books = changed_entries.limit(SYNC_ITEM_LIMIT)
log.debug("Books to Sync: {}".format(len(books.all())))
for book in books:
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)
reading_states_in_new_entitlements.append(book.Books.id)
- ts_created = book.Books.timestamp
+ ts_created = book.Books.timestamp.replace(tzinfo=None)
try:
ts_created = max(ts_created, book.date_added)
@@ -242,7 +242,7 @@ def HandleSyncRequest():
sync_results.append({"ChangedEntitlement": entitlement})
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:
new_books_last_modified = max(
@@ -254,27 +254,27 @@ def HandleSyncRequest():
new_books_last_created = max(ts_created, new_books_last_created)
kobo_sync_status.add_synced_books(book.Books.id)
- if sqlalchemy_version2:
+ '''if sqlalchemy_version2:
max_change = calibre_db.session.execute(changed_entries
.filter(ub.ArchivedBook.is_archived)
.filter(ub.ArchivedBook.user_id == current_user.id)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
.columns(db.Books).first()
- else:
- max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
- .filter(ub.ArchivedBook.user_id == current_user.id) \
- .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
+ else:'''
+ max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\
+ .filter(ub.ArchivedBook.user_id == current_user.id) \
+ .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
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)
# no. of books returned
- if sqlalchemy_version2:
+ '''if sqlalchemy_version2:
entries = calibre_db.session.execute(changed_entries).all()
book_count = len(entries)
- else:
- book_count = changed_entries.count()
+ else:'''
+ book_count = changed_entries.count()
# last entry:
cont_sync = bool(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)
- if sqlalchemy_version2:
+ '''if sqlalchemy_version2:
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
ub.Shelf.user_id == current_user.id,
*extra_filters
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf)
- else:
- shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
- or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
- func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
- ub.Shelf.user_id == current_user.id,
- *extra_filters
- ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
+ else:'''
+ shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
+ or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
+ func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),
+ ub.Shelf.user_id == current_user.id,
+ *extra_filters
+ ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
for shelf in shelflist:
if not shelf_lib.check_shelf_view_permissions(shelf):
diff --git a/cps/schedule.py b/cps/schedule.py
index cab8d7d3..05367e99 100644
--- a/cps/schedule.py
+++ b/cps/schedule.py
@@ -31,8 +31,8 @@ def get_scheduled_tasks(reconnect=True):
if reconnect:
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
- # ToDo make configurable. Generate metadata.opf file for each changed book
- if True:
+ # Generate metadata.opf file for each changed book
+ if config.schedule_metadata_backup:
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
# Generate all missing book cover thumbnails
diff --git a/cps/search.py b/cps/search.py
index 96f21d62..096b2928 100644
--- a/cps/search.py
+++ b/cps/search.py
@@ -35,13 +35,12 @@ search = Blueprint('search', __name__)
log = logger.create()
-@search.route("/search", methods=["POST"])
+@search.route("/search", methods=["GET"])
@login_required_if_no_ano
def simple_search():
- term = dict(request.form).get("query")
+ term = request.args.get("query")
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:
return render_title_template('search.html',
searchterm="",
diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py
index a53d7a99..c44841c1 100644
--- a/cps/services/SyncToken.py
+++ b/cps/services/SyncToken.py
@@ -20,7 +20,7 @@
import sys
from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
-from datetime import datetime
+from datetime import datetime, timezone
from urllib.parse import unquote
diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py
index 872538d1..dc915ceb 100644
--- a/cps/services/simpleldap.py
+++ b/cps/services/simpleldap.py
@@ -20,6 +20,7 @@ import base64
from flask_simpleldap import LDAP, LDAPException
from flask_simpleldap import ldap as pyLDAP
+from flask import current_app
from .. import constants, logger
try:
@@ -28,8 +29,47 @@ except ImportError:
pass
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):
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_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
-
+ app.config['LDAP_LOGLEVEL'] = config.config_log_level
try:
_ldap.init_app(app)
except ValueError:
diff --git a/cps/shelf.py b/cps/shelf.py
index 04f7a6a6..5d05cfe2 100644
--- a/cps/shelf.py
+++ b/cps/shelf.py
@@ -295,11 +295,14 @@ def check_shelf_edit_permissions(cur_shelf):
def check_shelf_view_permissions(cur_shelf):
- if cur_shelf.is_public:
- return True
- if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
- log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
- return False
+ try:
+ if cur_shelf.is_public:
+ return True
+ if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
+ log.error("User is unauthorized to view non-public shelf: {}".format(cur_shelf.name))
+ return False
+ except Exception as e:
+ log.error(e)
return True
diff --git a/cps/static/js/caliBlur.js b/cps/static/js/caliBlur.js
index ec394d0b..cc4116cf 100755
--- a/cps/static/js/caliBlur.js
+++ b/cps/static/js/caliBlur.js
@@ -314,9 +314,6 @@ $(document).mouseup(function (e) {
});
});
-// Split path name to array and remove blanks
-url = window.location.pathname
-
// Move create shelf
$("#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
$("div.comments").readmore({
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.
backurl = "../../book/" + url[2]
$("body.epub #title-controls")
@@ -529,6 +503,7 @@ if ($("body.shelf").length > 0) {
// Rest of Tooltips
$(".home-btn > a").attr({
"data-toggle": "tooltip",
+ "href": $(".navbar-brand")[0].href,
"title": $(document.body).attr("data-text"), // Home
"data-placement": "bottom"
})
diff --git a/cps/static/js/details.js b/cps/static/js/details.js
index f0259f8c..24b98437 100644
--- a/cps/static/js/details.js
+++ b/cps/static/js/details.js
@@ -1,5 +1,5 @@
/* 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
* it under the terms of the GNU General Public License as published by
@@ -17,6 +17,35 @@
/* 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('
' + item.message + '
');
+ });
+ } else {
+ data.forEach(function (item) {
+ $(".navbar").after('' +
+ '
' + item.message + '
' +
+ '
');
+ });
+ }
+ }
+}
+$(".sendbtn-form").click(function() {
+ $.ajax({
+ method: 'post',
+ url: $(this).data('href'),
+ success: function (data) {
+ handleResponse(data)
+ }
+ })
+});
+
$(function() {
$("#have_read_form").ajaxForm();
});
diff --git a/cps/static/js/logviewer.js b/cps/static/js/logviewer.js
index 7b93f30f..9d3395cc 100644
--- a/cps/static/js/logviewer.js
+++ b/cps/static/js/logviewer.js
@@ -36,7 +36,7 @@ function init(logType) {
d.innerHTML = "loading ...";
$.ajax({
- url: window.location.pathname + "/../../ajax/log/" + logType,
+ url: getPath() + "/ajax/log/" + logType,
datatype: "text",
cache: false
})
diff --git a/cps/static/js/main.js b/cps/static/js/main.js
index de656bd6..8d7354ef 100644
--- a/cps/static/js/main.js
+++ b/cps/static/js/main.js
@@ -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) {
// $(".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
$(".container-fluid").bind("dragenter dragover", function () {
if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
@@ -313,7 +304,7 @@ $(function() {
}
function fillFileTable(path, type, folder, filt) {
- var request_path = "/../../ajax/pathchooser/";
+ var request_path = "/ajax/pathchooser/";
$.ajax({
dataType: "json",
data: {
@@ -321,7 +312,7 @@ $(function() {
folder: folder,
filter: filt
},
- url: window.location.pathname + request_path,
+ url: getPath() + request_path,
success: function success(data) {
if ($("#element_selected").text() ==="") {
$("#element_selected").text(data.cwd);
@@ -434,7 +425,7 @@ $(function() {
}
$.ajax({
dataType: "json",
- url: window.location.pathname + "/../../get_update_status",
+ url: getPath() + "/get_update_status",
success: function success(data) {
$this.html(buttonText);
@@ -538,6 +529,7 @@ $(function() {
$("#bookDetailsModal")
.on("show.bs.modal", function(e) {
$("#flash_danger").remove();
+ $("#flash_success").remove();
var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times
@@ -650,7 +642,6 @@ $(function() {
);
});
-
$("#user_submit").click(function() {
this.closest("form").submit();
});
@@ -682,7 +673,7 @@ $(function() {
$.ajax({
method:"post",
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()},
success: function success(data) {
if ( data.change ) {
@@ -709,17 +700,16 @@ $(function() {
e.stopPropagation();
this.blur();
window.scrollTo({top: 0, behavior: 'smooth'});
- var request_path = "/../../admin/ajaxconfig";
- var loader = "/../..";
+ var request_path = "/admin/ajaxconfig";
$("#flash_success").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);
if(data.reboot) {
$("#spinning_success").show();
var rebootInterval = setInterval(function(){
$.get({
- url:window.location.pathname + "/../../admin/alive",
+ url:getPath() + "/admin/alive",
success: function (d, statusText, xhr) {
if (xhr.status < 400) {
$("#spinning_success").hide();
@@ -745,7 +735,6 @@ $(function() {
$(this).data('value'),
function(value){
postButton(event, $("#delete_shelf").data("action"));
- // $("#delete_shelf").closest("form").submit()
}
);
diff --git a/cps/static/js/table.js b/cps/static/js/table.js
index 833f1a13..36361c3c 100644
--- a/cps/static/js/table.js
+++ b/cps/static/js/table.js
@@ -49,7 +49,7 @@ $(function() {
method: "post",
contentType: "application/json; charset=utf-8",
dataType: "json",
- url: window.location.pathname + "/../ajax/canceltask",
+ url: getPath() + "/ajax/canceltask",
data: JSON.stringify({"task_id": taskId}),
});
});
diff --git a/cps/tasks/metadata_backup.py b/cps/tasks/metadata_backup.py
index 162d4852..1751feeb 100644
--- a/cps/tasks/metadata_backup.py
+++ b/cps/tasks/metadata_backup.py
@@ -17,10 +17,9 @@
# along with this program. If not, see .
import os
-from lxml import objectify
from urllib.request import urlopen
from lxml import etree
-from html import escape
+
from cps import config, db, gdriveutils, logger
from cps.services.worker import CalibreTask
@@ -102,50 +101,29 @@ class TaskBackupMetadata(CalibreTask):
self.calibre_db.session.close()
def open_metadata(self, book, custom_columns):
+ package = self.create_new_metadata_backup(book, custom_columns)
if config.config_use_google_drive:
if not gdriveutils.is_gdrive_ready():
raise Exception('Google Drive is configured but not ready')
- web_content_link = gdriveutils.get_metadata_backup_via_gdrive(book.path)
- if not web_content_link:
- raise Exception('Google Drive cover url not found')
-
- stream = None
- try:
- 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()
+ gdriveutils.uploadFileToEbooksFolder(os.path.join(book.path, 'metadata.opf').replace("\\", "/"),
+ etree.tostring(package,
+ xml_declaration=True,
+ encoding='utf-8',
+ pretty_print=True).decode('utf-8'),
+ True)
else:
# ToDo: Handle book folder not found or not readable
book_metadata_filepath = os.path.join(config.config_calibre_dir, book.path, 'metadata.opf')
- #if not os.path.isfile(book_metadata_filepath):
- self.create_new_metadata_backup(book, custom_columns, book_metadata_filepath)
- # else:
- '''namespaces = {'dc': PURL_NAMESPACE, 'opf': OPF_NAMESPACE}
- test = etree.parse(book_metadata_filepath)
- root = test.getroot()
- for i in root.iter():
- self.log.info(i)
- title = root.find("dc:metadata", namespaces)
- pass
- with open(book_metadata_filepath, "rb") as f:
- xml = f.read()
+ # prepare finalize everything and output
+ doc = etree.ElementTree(package)
+ 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))
- root = objectify.fromstring(xml)
- # 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):
+ def create_new_metadata_backup(self, book, custom_columns):
# generate root package element
package = etree.Element(OPF + "package", nsmap=OPF_NS)
package.set("unique-identifier", "uuid_id")
@@ -230,14 +208,7 @@ class TaskBackupMetadata(CalibreTask):
guide = etree.SubElement(package, "guide")
etree.SubElement(guide, "reference", type="cover", title=self.translated_title, href="cover.jpg")
- # prepare finalize everything and output
- doc = etree.ElementTree(package)
- # doc = etree.tostring(package, xml_declaration=True, encoding='utf-8', pretty_print=True) # .replace(b""", 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))
+ return package
@property
def name(self):
diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py
index e66da036..6d11fe97 100644
--- a/cps/tasks/thumbnail.py
+++ b/cps/tasks/thumbnail.py
@@ -138,7 +138,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
# Replace outdated or missing 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
self.update_book_cover_thumbnail(book, thumbnail)
diff --git a/cps/tasks_status.py b/cps/tasks_status.py
index 7043483c..fc3c9914 100644
--- a/cps/tasks_status.py
+++ b/cps/tasks_status.py
@@ -43,9 +43,7 @@ def get_email_status_json():
@login_required
def get_tasks_status():
# if current user admin, show all email, otherwise only own emails
- tasks = WorkerThread.get_instance().tasks
- answer = render_task_status(tasks)
- return render_title_template('tasks.html', entries=answer, title=_("Tasks"), page="tasks")
+ return render_title_template('tasks.html', title=_("Tasks"), page="tasks")
# helper function to apply localize status information in tasklist entries
diff --git a/cps/templates/admin.html b/cps/templates/admin.html
index 9460fa83..ac124fe8 100644
--- a/cps/templates/admin.html
+++ b/cps/templates/admin.html
@@ -186,6 +186,10 @@
{{_('Reconnect Calibre Database')}}
{{ display_bool_setting(config.schedule_reconnect) }}
+
+
{{_('Generate Metadata Backup Files')}}
+
{{ display_bool_setting(config.schedule_metadata_backup) }}
+
{{_('Edit Scheduled Tasks Settings')}}
@@ -207,10 +211,11 @@
{{_('Restart')}}
{{_('Shutdown')}}
+{% if config.schedule_metadata_backup %}
-
+{% endif %}
{{_('Version Information')}}
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html
index 265ceff3..b11ca1b5 100644
--- a/cps/templates/config_edit.html
+++ b/cps/templates/config_edit.html
@@ -358,7 +358,7 @@
diff --git a/cps/templates/detail.html b/cps/templates/detail.html
old mode 100644
new mode 100755
index c9cb8143..62ba5c4e
--- a/cps/templates/detail.html
+++ b/cps/templates/detail.html
@@ -1,326 +1,369 @@
{% extends is_xhr|yesno("fragment.html", "layout.html") %}
{% block body %}
-
-
-
-
-
-
-
-
{{ _('Description:') }}
+ {{ entry.comments[0].text|safe }} +