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"&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)) + 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 %}
{{_('Queue all books for 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 @@

- {{_('Securitiy Settings')}} + {{_('Security Settings')}}

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 %}
-
-
-
- - -
-
-
- {% endblock %} {% block js %} + + {% endblock %} + diff --git a/cps/templates/layout.html b/cps/templates/layout.html index f6423bb3..07adbabd 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -41,8 +41,7 @@
{% endif %} {% if current_user.is_authenticated or g.allow_anonymous %} -