From 26071d4e7ad1943f91904804fc1b5464866d748b Mon Sep 17 00:00:00 2001 From: mmonkey Date: Sun, 26 Sep 2021 02:02:48 -0500 Subject: [PATCH] Added Scheduled Tasks Settings --- cps/admin.py | 62 +++++++++++++++++++--------- cps/config_sql.py | 15 +++++-- cps/schedule.py | 19 +++++---- cps/services/background_scheduler.py | 24 +++++++---- cps/tasks/thumbnail.py | 14 +------ cps/templates/admin.html | 43 +++++++++++-------- cps/templates/schedule_edit.html | 38 +++++++++++++++++ 7 files changed, 147 insertions(+), 68 deletions(-) create mode 100644 cps/templates/schedule_edit.html diff --git a/cps/admin.py b/cps/admin.py index 92c8bd70..bd292ba3 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -159,23 +159,6 @@ def shutdown(): return json.dumps(showtext), 400 -@admi.route("/clear-cache") -@login_required -@admin_required -def clear_cache(): - cache_type = request.args.get('cache_type'.strip()) - showtext = {} - - if cache_type == constants.CACHE_TYPE_THUMBNAILS: - log.info('clearing cover thumbnail cache') - showtext['text'] = _(u'Cleared cover thumbnail cache') - helper.clear_cover_thumbnail_cache() - return json.dumps(showtext) - - showtext['text'] = _(u'Unknown command') - return json.dumps(showtext) - - @admi.route("/admin/view") @login_required @admin_required @@ -205,6 +188,7 @@ def admin(): feature_support=feature_support, kobo_support=kobo_support, title=_(u"Admin page"), page="admin") + @admi.route("/admin/dbconfig", methods=["GET", "POST"]) @login_required @admin_required @@ -245,6 +229,7 @@ def ajax_db_config(): def calibreweb_alive(): return "", 200 + @admi.route("/admin/viewconfig") @login_required @admin_required @@ -257,6 +242,7 @@ def view_configuration(): restrictColumns=restrict_columns, title=_(u"UI Configuration"), page="uiconfig") + @admi.route("/admin/usertable") @login_required @admin_required @@ -339,6 +325,7 @@ def list_users(): response.headers["Content-Type"] = "application/json; charset=utf-8" return response + @admi.route("/ajax/deleteuser", methods=['POST']) @login_required @admin_required @@ -372,6 +359,7 @@ def delete_user(): success.extend(errors) return Response(json.dumps(success), mimetype='application/json') + @admi.route("/ajax/getlocale") @login_required @admin_required @@ -517,6 +505,7 @@ def update_table_settings(): return "Invalid request", 400 return "" + def check_valid_read_column(column): if column != "0": if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ @@ -524,6 +513,7 @@ def check_valid_read_column(column): return False return True + def check_valid_restricted_column(column): if column != "0": if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ @@ -532,7 +522,6 @@ def check_valid_restricted_column(column): return True - @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @admin_required @@ -564,7 +553,6 @@ def update_view_configuration(): _config_int(to_save, "config_books_per_page") _config_int(to_save, "config_authors_max") - config.config_default_role = constants.selected_roles(to_save) config.config_default_role &= ~constants.ROLE_ANONYMOUS @@ -1210,6 +1198,7 @@ def _db_configuration_update_helper(): config.save() return _db_configuration_result(None, gdrive_error) + def _configuration_update_helper(): reboot_required = False to_save = request.form.to_dict() @@ -1299,6 +1288,7 @@ def _configuration_update_helper(): return _configuration_result(None, reboot_required) + def _configuration_result(error_flash=None, reboot=False): resp = {} if error_flash: @@ -1388,6 +1378,7 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): log.error("Settings DB is not Writeable") flash(_("Settings DB is not Writeable"), category="error") + def _delete_user(content): if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count(): @@ -1572,6 +1563,39 @@ def update_mailsettings(): return edit_mailsettings() +@admi.route("/admin/scheduledtasks") +@login_required +@admin_required +def edit_scheduledtasks(): + content = config.get_scheduled_task_settings() + return render_title_template("schedule_edit.html", content=content, title=_(u"Edit Scheduled Tasks Settings")) + + +@admi.route("/admin/scheduledtasks", methods=["POST"]) +@login_required +@admin_required +def update_scheduledtasks(): + to_save = request.form.to_dict() + _config_int(to_save, "schedule_start_time") + _config_int(to_save, "schedule_end_time") + _config_checkbox(to_save, "schedule_generate_book_covers") + _config_checkbox(to_save, "schedule_generate_series_covers") + + try: + config.save() + flash(_(u"Scheduled tasks settings updated"), category="success") + except IntegrityError as ex: + ub.session.rollback() + log.error("An unknown error occurred while saving scheduled tasks settings") + flash(_(u"An unknown error occurred. Please try again later."), category="error") + except OperationalError: + ub.session.rollback() + log.error("Settings DB is not Writeable") + flash(_("Settings DB is not Writeable"), category="error") + + return edit_scheduledtasks() + + @admi.route("/admin/user/", methods=["GET", "POST"]) @login_required @admin_required diff --git a/cps/config_sql.py b/cps/config_sql.py index 88107f9b..4841f03d 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -133,13 +133,18 @@ class _Settings(_Base): config_calibre = Column(String) config_rarfile_location = Column(String, default=None) config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD)) - config_unicode_filename =Column(Boolean, default=False) + config_unicode_filename = Column(Boolean, default=False) config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE) config_reverse_proxy_login_header_name = Column(String) config_allow_reverse_proxy_header_login = Column(Boolean, default=False) + schedule_start_time = Column(Integer, default=4) + schedule_end_time = Column(Integer, default=6) + schedule_generate_book_covers = Column(Boolean, default=False) + schedule_generate_series_covers = Column(Boolean, default=False) + def __repr__(self): return self.__class__.__name__ @@ -170,7 +175,6 @@ class _ConfigSQL(object): if change: self.save() - def _read_from_storage(self): if self._settings is None: log.debug("_ConfigSQL._read_from_storage") @@ -254,6 +258,8 @@ class _ConfigSQL(object): return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0) or (self.mail_gmail_token != {} and self.mail_server_type == 1)) + def get_scheduled_task_settings(self): + return {k:v for k, v in self.__dict__.items() if k.startswith('schedule_')} def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None): """Possibly updates a field of this object. @@ -289,7 +295,6 @@ class _ConfigSQL(object): storage[k] = v return storage - def load(self): '''Load all configuration values from the underlying storage.''' s = self._read_from_storage() # type: _Settings @@ -407,6 +412,7 @@ def autodetect_calibre_binary(): return element return "" + def autodetect_unrar_binary(): if sys.platform == "win32": calibre_path = ["C:\\program files\\WinRar\\unRAR.exe", @@ -418,6 +424,7 @@ def autodetect_unrar_binary(): return element return "" + def autodetect_kepubify_binary(): if sys.platform == "win32": calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe", @@ -429,6 +436,7 @@ def autodetect_kepubify_binary(): return element return "" + def _migrate_database(session): # make sure the table is created, if it does not exist _Base.metadata.create_all(session.bind) @@ -452,6 +460,7 @@ def load_configuration(session): # session.commit() return conf + def get_flask_session_key(session): flask_settings = session.query(_Flask_Settings).one_or_none() if flask_settings == None: diff --git a/cps/schedule.py b/cps/schedule.py index dc153b9a..2cddaecb 100644 --- a/cps/schedule.py +++ b/cps/schedule.py @@ -19,7 +19,6 @@ from __future__ import division, print_function, unicode_literals from .services.background_scheduler import BackgroundScheduler -from .services.worker import WorkerThread from .tasks.database import TaskReconnectDatabase from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails @@ -28,13 +27,19 @@ def register_jobs(): scheduler = BackgroundScheduler() if scheduler: - # Reconnect metadata.db once every 12 hours - scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16') + # Reconnect Calibre database (metadata.db) + scheduler.schedule_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16') - # Generate all missing book cover thumbnails once every 24 hours - scheduler.add_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4) + # Generate all missing book cover thumbnails + scheduler.schedule_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4) + + # Generate all missing series thumbnails + scheduler.schedule_task(user=None, task=lambda: TaskGenerateSeriesThumbnails(), trigger='cron', hour=4) def register_startup_jobs(): - WorkerThread.add(None, TaskGenerateCoverThumbnails()) - # WorkerThread.add(None, TaskGenerateSeriesThumbnails()) + scheduler = BackgroundScheduler() + + if scheduler: + scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateCoverThumbnails()) + scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateSeriesThumbnails()) diff --git a/cps/services/background_scheduler.py b/cps/services/background_scheduler.py index 1b588ac2..ba578903 100644 --- a/cps/services/background_scheduler.py +++ b/cps/services/background_scheduler.py @@ -40,25 +40,31 @@ class BackgroundScheduler: if cls._instance is None: cls._instance = super(BackgroundScheduler, cls).__new__(cls) - - scheduler = BScheduler() - atexit.register(lambda: scheduler.shutdown()) - cls.log = logger.create() - cls.scheduler = scheduler + cls.scheduler = BScheduler() cls.scheduler.start() + atexit.register(lambda: cls.scheduler.shutdown()) + return cls._instance - def add(self, func, trigger, **trigger_args): + def _add(self, func, trigger, **trigger_args): if use_APScheduler: return self.scheduler.add_job(func=func, trigger=trigger, **trigger_args) - def add_task(self, user, task, trigger, **trigger_args): + # Expects a lambda expression for the task, so that the task isn't instantiated before the task is scheduled + def schedule_task(self, user, task, trigger, **trigger_args): if use_APScheduler: def scheduled_task(): worker_task = task() - self.log.info(f'Running scheduled task in background: {worker_task.name} - {worker_task.message}') WorkerThread.add(user, worker_task) - return self.add(func=scheduled_task, trigger=trigger, **trigger_args) + return self._add(func=scheduled_task, trigger=trigger, **trigger_args) + + # Expects a lambda expression for the task, so that the task isn't instantiated before the task is scheduled + def schedule_task_immediately(self, user, task): + if use_APScheduler: + def scheduled_task(): + WorkerThread.add(user, task()) + + return self._add(func=scheduled_task, trigger='date') diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py index 152b8772..a220fd8c 100644 --- a/cps/tasks/thumbnail.py +++ b/cps/tasks/thumbnail.py @@ -167,9 +167,9 @@ class TaskGenerateCoverThumbnails(CalibreTask): try: stream = urlopen(web_content_link) with Image(file=stream) as img: - height = self.get_thumbnail_height(thumbnail) + height = get_resize_height(thumbnail.resolution) if img.height > height: - width = self.get_thumbnail_width(height, img) + width = get_resize_width(thumbnail.resolution, img.width, img.height) img.resize(width=width, height=height, filter='lanczos') img.format = thumbnail.format filename = self.cache.get_cache_file_path(thumbnail.filename, @@ -212,16 +212,6 @@ class TaskGenerateSeriesThumbnails(CalibreTask): constants.COVER_THUMBNAIL_MEDIUM ] - # get all series - # get all books in series with covers and count >= 4 books - # get the dimensions from the first book in the series & pop the first book from the series list of books - # randomly select three other books in the series - - # resize the covers in the sequence? - # create an image sequence from the 4 selected books of the series - # join pairs of books in the series with wand's concat - # join the two sets of pairs with wand's - def run(self, worker_thread): if self.calibre_db.session and use_IM: all_series = self.get_series_with_four_plus_books() diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 597ba103..ec0fc84e 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -156,6 +156,31 @@ +
+
+

{{_('Scheduled Tasks')}}

+
+
+
{{_('Time at which tasks start to run')}}
+
{{config.schedule_start_time}}:00
+
+
+
{{_('Time at which tasks stop running')}}
+
{{config.schedule_end_time}}:00
+
+
+
{{_('Generate book cover thumbnails')}}
+
{{ display_bool_setting(config.schedule_generate_book_covers) }}
+
+
+
{{_('Generate series cover thumbnails')}}
+
{{ display_bool_setting(config.schedule_generate_series_covers) }}
+
+
+ {{_('Edit Scheduled Tasks Settings')}} +
+
+

{{_('Administration')}}

{{_('Download Debug Package')}} @@ -163,7 +188,6 @@
{{_('Reconnect Calibre Database')}}
-
{{_('Clear Cover Thumbnail Cache')}}
{{_('Restart')}}
@@ -248,21 +272,4 @@
- {% endblock %} diff --git a/cps/templates/schedule_edit.html b/cps/templates/schedule_edit.html new file mode 100644 index 00000000..f4e72224 --- /dev/null +++ b/cps/templates/schedule_edit.html @@ -0,0 +1,38 @@ +{% extends "layout.html" %} +{% block header %} + + +{% endblock %} +{% block body %} +
+

{{title}}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {{_('Cancel')}} +
+
+{% endblock %}