1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-18 14:10:30 +00:00

Added Scheduled Tasks Settings

This commit is contained in:
mmonkey 2021-09-26 02:02:48 -05:00
parent 0bd544704d
commit 26071d4e7a
7 changed files with 147 additions and 68 deletions

View File

@ -159,23 +159,6 @@ def shutdown():
return json.dumps(showtext), 400 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") @admi.route("/admin/view")
@login_required @login_required
@admin_required @admin_required
@ -205,6 +188,7 @@ def admin():
feature_support=feature_support, kobo_support=kobo_support, feature_support=feature_support, kobo_support=kobo_support,
title=_(u"Admin page"), page="admin") title=_(u"Admin page"), page="admin")
@admi.route("/admin/dbconfig", methods=["GET", "POST"]) @admi.route("/admin/dbconfig", methods=["GET", "POST"])
@login_required @login_required
@admin_required @admin_required
@ -245,6 +229,7 @@ def ajax_db_config():
def calibreweb_alive(): def calibreweb_alive():
return "", 200 return "", 200
@admi.route("/admin/viewconfig") @admi.route("/admin/viewconfig")
@login_required @login_required
@admin_required @admin_required
@ -257,6 +242,7 @@ def view_configuration():
restrictColumns=restrict_columns, restrictColumns=restrict_columns,
title=_(u"UI Configuration"), page="uiconfig") title=_(u"UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable") @admi.route("/admin/usertable")
@login_required @login_required
@admin_required @admin_required
@ -339,6 +325,7 @@ def list_users():
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response return response
@admi.route("/ajax/deleteuser", methods=['POST']) @admi.route("/ajax/deleteuser", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@ -372,6 +359,7 @@ def delete_user():
success.extend(errors) success.extend(errors)
return Response(json.dumps(success), mimetype='application/json') return Response(json.dumps(success), mimetype='application/json')
@admi.route("/ajax/getlocale") @admi.route("/ajax/getlocale")
@login_required @login_required
@admin_required @admin_required
@ -517,6 +505,7 @@ def update_table_settings():
return "Invalid request", 400 return "Invalid request", 400
return "" return ""
def check_valid_read_column(column): def check_valid_read_column(column):
if column != "0": if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ 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 False
return True return True
def check_valid_restricted_column(column): def check_valid_restricted_column(column):
if column != "0": if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ 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 return True
@admi.route("/admin/viewconfig", methods=["POST"]) @admi.route("/admin/viewconfig", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
@ -564,7 +553,6 @@ def update_view_configuration():
_config_int(to_save, "config_books_per_page") _config_int(to_save, "config_books_per_page")
_config_int(to_save, "config_authors_max") _config_int(to_save, "config_authors_max")
config.config_default_role = constants.selected_roles(to_save) config.config_default_role = constants.selected_roles(to_save)
config.config_default_role &= ~constants.ROLE_ANONYMOUS config.config_default_role &= ~constants.ROLE_ANONYMOUS
@ -1210,6 +1198,7 @@ def _db_configuration_update_helper():
config.save() config.save()
return _db_configuration_result(None, gdrive_error) return _db_configuration_result(None, gdrive_error)
def _configuration_update_helper(): def _configuration_update_helper():
reboot_required = False reboot_required = False
to_save = request.form.to_dict() to_save = request.form.to_dict()
@ -1299,6 +1288,7 @@ def _configuration_update_helper():
return _configuration_result(None, reboot_required) return _configuration_result(None, reboot_required)
def _configuration_result(error_flash=None, reboot=False): def _configuration_result(error_flash=None, reboot=False):
resp = {} resp = {}
if error_flash: 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") log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error") flash(_("Settings DB is not Writeable"), category="error")
def _delete_user(content): def _delete_user(content):
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count(): ub.User.id != content.id).count():
@ -1572,6 +1563,39 @@ def update_mailsettings():
return edit_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/<int:user_id>", methods=["GET", "POST"]) @admi.route("/admin/user/<int:user_id>", methods=["GET", "POST"])
@login_required @login_required
@admin_required @admin_required

View File

@ -133,13 +133,18 @@ class _Settings(_Base):
config_calibre = Column(String) config_calibre = Column(String)
config_rarfile_location = Column(String, default=None) config_rarfile_location = Column(String, default=None)
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD)) 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_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
config_reverse_proxy_login_header_name = Column(String) config_reverse_proxy_login_header_name = Column(String)
config_allow_reverse_proxy_header_login = Column(Boolean, default=False) 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): def __repr__(self):
return self.__class__.__name__ return self.__class__.__name__
@ -170,7 +175,6 @@ class _ConfigSQL(object):
if change: if change:
self.save() self.save()
def _read_from_storage(self): def _read_from_storage(self):
if self._settings is None: if self._settings is None:
log.debug("_ConfigSQL._read_from_storage") 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) 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)) 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): def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
"""Possibly updates a field of this object. """Possibly updates a field of this object.
@ -289,7 +295,6 @@ class _ConfigSQL(object):
storage[k] = v storage[k] = v
return storage return storage
def load(self): def load(self):
'''Load all configuration values from the underlying storage.''' '''Load all configuration values from the underlying storage.'''
s = self._read_from_storage() # type: _Settings s = self._read_from_storage() # type: _Settings
@ -407,6 +412,7 @@ def autodetect_calibre_binary():
return element return element
return "" return ""
def autodetect_unrar_binary(): def autodetect_unrar_binary():
if sys.platform == "win32": if sys.platform == "win32":
calibre_path = ["C:\\program files\\WinRar\\unRAR.exe", calibre_path = ["C:\\program files\\WinRar\\unRAR.exe",
@ -418,6 +424,7 @@ def autodetect_unrar_binary():
return element return element
return "" return ""
def autodetect_kepubify_binary(): def autodetect_kepubify_binary():
if sys.platform == "win32": if sys.platform == "win32":
calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe", calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe",
@ -429,6 +436,7 @@ def autodetect_kepubify_binary():
return element return element
return "" return ""
def _migrate_database(session): def _migrate_database(session):
# make sure the table is created, if it does not exist # make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind) _Base.metadata.create_all(session.bind)
@ -452,6 +460,7 @@ def load_configuration(session):
# session.commit() # session.commit()
return conf return conf
def get_flask_session_key(session): def get_flask_session_key(session):
flask_settings = session.query(_Flask_Settings).one_or_none() flask_settings = session.query(_Flask_Settings).one_or_none()
if flask_settings == None: if flask_settings == None:

View File

@ -19,7 +19,6 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
from .services.background_scheduler import BackgroundScheduler from .services.background_scheduler import BackgroundScheduler
from .services.worker import WorkerThread
from .tasks.database import TaskReconnectDatabase from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails
@ -28,13 +27,19 @@ def register_jobs():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
if scheduler: if scheduler:
# Reconnect metadata.db once every 12 hours # Reconnect Calibre database (metadata.db)
scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16') scheduler.schedule_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16')
# Generate all missing book cover thumbnails once every 24 hours # Generate all missing book cover thumbnails
scheduler.add_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4) 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(): def register_startup_jobs():
WorkerThread.add(None, TaskGenerateCoverThumbnails()) scheduler = BackgroundScheduler()
# WorkerThread.add(None, TaskGenerateSeriesThumbnails())
if scheduler:
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateCoverThumbnails())
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateSeriesThumbnails())

View File

@ -40,25 +40,31 @@ class BackgroundScheduler:
if cls._instance is None: if cls._instance is None:
cls._instance = super(BackgroundScheduler, cls).__new__(cls) cls._instance = super(BackgroundScheduler, cls).__new__(cls)
scheduler = BScheduler()
atexit.register(lambda: scheduler.shutdown())
cls.log = logger.create() cls.log = logger.create()
cls.scheduler = scheduler cls.scheduler = BScheduler()
cls.scheduler.start() cls.scheduler.start()
atexit.register(lambda: cls.scheduler.shutdown())
return cls._instance return cls._instance
def add(self, func, trigger, **trigger_args): def _add(self, func, trigger, **trigger_args):
if use_APScheduler: if use_APScheduler:
return self.scheduler.add_job(func=func, trigger=trigger, **trigger_args) 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: if use_APScheduler:
def scheduled_task(): def scheduled_task():
worker_task = task() worker_task = task()
self.log.info(f'Running scheduled task in background: {worker_task.name} - {worker_task.message}')
WorkerThread.add(user, worker_task) 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')

View File

@ -167,9 +167,9 @@ class TaskGenerateCoverThumbnails(CalibreTask):
try: try:
stream = urlopen(web_content_link) stream = urlopen(web_content_link)
with Image(file=stream) as img: with Image(file=stream) as img:
height = self.get_thumbnail_height(thumbnail) height = get_resize_height(thumbnail.resolution)
if img.height > height: 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.resize(width=width, height=height, filter='lanczos')
img.format = thumbnail.format img.format = thumbnail.format
filename = self.cache.get_cache_file_path(thumbnail.filename, filename = self.cache.get_cache_file_path(thumbnail.filename,
@ -212,16 +212,6 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
constants.COVER_THUMBNAIL_MEDIUM 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): def run(self, worker_thread):
if self.calibre_db.session and use_IM: if self.calibre_db.session and use_IM:
all_series = self.get_series_with_four_plus_books() all_series = self.get_series_with_four_plus_books()

View File

@ -156,6 +156,31 @@
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<h2>{{_('Scheduled Tasks')}}</h2>
<div class="col-xs-12 col-sm-12">
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks start to run')}}</div>
<div class="col-xs-6 col-sm-3">{{config.schedule_start_time}}:00</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks stop running')}}</div>
<div class="col-xs-6 col-sm-3">{{config.schedule_end_time}}:00</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('Generate book cover thumbnails')}}</div>
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_book_covers) }}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('Generate series cover thumbnails')}}</div>
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_series_covers) }}</div>
</div>
</div>
<a class="btn btn-default scheduledtasks" id="admin_edit_scheduled_tasks" href="{{url_for('admin.edit_scheduledtasks')}}">{{_('Edit Scheduled Tasks Settings')}}</a>
</div>
</div>
<div class="row form-group"> <div class="row form-group">
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<a class="btn btn-default" id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a> <a class="btn btn-default" id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a>
@ -163,7 +188,6 @@
</div> </div>
<div class="row form-group"> <div class="row form-group">
<div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div> <div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div>
<div class="btn btn-default" id="clear_cover_thumbnail_cache" data-toggle="modal" data-target="#ClearCacheDialog">{{_('Clear Cover Thumbnail Cache')}}</div>
</div> </div>
<div class="row form-group"> <div class="row form-group">
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div> <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
@ -248,21 +272,4 @@
</div> </div>
</div> </div>
</div> </div>
<div id="ClearCacheDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-info"></div>
<div class="modal-body text-center">
<p>{{_('Are you sure you want to clear the cover thumbnail cache?')}}</p>
<div id="spinner3" class="spinner" style="display:none;">
<img id="img-spinner3" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/>
</div>
<p></p>
<button type="button" class="btn btn-default" id="clear_cache" >{{_('OK')}}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "layout.html" %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
{% endblock %}
{% block body %}
<div class="discover">
<h1>{{title}}</h1>
<form role="form" class="col-md-10 col-lg-6" method="POST" autocomplete="off">
<div class="form-group">
<label for="schedule_start_time">{{_('Time at which tasks start to run')}}</label>
<select name="schedule_start_time" id="schedule_start_time" class="form-control">
{% for n in range(24) %}
<option value="{{n}}" {% if content.schedule_start_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="schedule_end_time">{{_('Time at which tasks stop running')}}</label>
<select name="schedule_end_time" id="schedule_end_time" class="form-control">
{% for n in range(24) %}
<option value="{{n}}" {% if content.schedule_end_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<input type="checkbox" id="schedule_generate_book_covers" name="schedule_generate_book_covers" checked>
<label for="schedule_generate_book_covers">{{_('Generate Book Cover Thumbnails')}}</label>
</div>
<div class="form-group">
<input type="checkbox" id="schedule_generate_series_covers" name="schedule_generate_series_covers" {% if config.schedule_generate_series_covers %}checked{% endif %}>
<label for="schedule_generate_series_covers">{{_('Generate Series Cover Thumbnails')}}</label>
</div>
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Cancel')}}</a>
</form>
</div>
{% endblock %}