Made long running tasks cancellable. Added cancel button to cancellable tasks in the task list. Added APP_MODE env variable for determining if the app is running in development, test, or production.

pull/1771/head
mmonkey 3 years ago
parent 26071d4e7a
commit 46205a1f83

@ -44,7 +44,7 @@ from cps.editbooks import editbook
from cps.remotelogin import remotelogin from cps.remotelogin import remotelogin
from cps.search_metadata import meta from cps.search_metadata import meta
from cps.error_handler import init_errorhandler from cps.error_handler import init_errorhandler
from cps.schedule import register_jobs, register_startup_jobs from cps.schedule import register_scheduled_tasks, register_startup_tasks
try: try:
from cps.kobo import kobo, get_kobo_activated from cps.kobo import kobo, get_kobo_activated
@ -81,9 +81,9 @@ def main():
if oauth_available: if oauth_available:
app.register_blueprint(oauth) app.register_blueprint(oauth)
# Register scheduled jobs # Register scheduled tasks
register_jobs() register_scheduled_tasks()
# register_startup_jobs() register_startup_tasks()
success = web_server.start() success = web_server.start()
sys.exit(0 if success else 1) sys.exit(0 if success else 1)

@ -40,12 +40,13 @@ from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services, isoLanguages, fs from . import constants, logger, helper, services, isoLanguages
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, schedule
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
valid_email, check_username valid_email, check_username
from .gdriveutils import is_gdrive_ready, gdrive_support from .gdriveutils import is_gdrive_ready, gdrive_support
from .render_template import render_title_template, get_sidebar_config from .render_template import render_title_template, get_sidebar_config
from .services.worker import WorkerThread
from . import debug_info, _BABEL_TRANSLATIONS from . import debug_info, _BABEL_TRANSLATIONS
try: try:
@ -1568,7 +1569,7 @@ def update_mailsettings():
@admin_required @admin_required
def edit_scheduledtasks(): def edit_scheduledtasks():
content = config.get_scheduled_task_settings() content = config.get_scheduled_task_settings()
return render_title_template("schedule_edit.html", content=content, title=_(u"Edit Scheduled Tasks Settings")) return render_title_template("schedule_edit.html", config=content, title=_(u"Edit Scheduled Tasks Settings"))
@admi.route("/admin/scheduledtasks", methods=["POST"]) @admi.route("/admin/scheduledtasks", methods=["POST"])
@ -1584,6 +1585,12 @@ def update_scheduledtasks():
try: try:
config.save() config.save()
flash(_(u"Scheduled tasks settings updated"), category="success") flash(_(u"Scheduled tasks settings updated"), category="success")
# Cancel any running tasks
schedule.end_scheduled_tasks()
# Re-register tasks with new settings
schedule.register_scheduled_tasks()
except IntegrityError as ex: except IntegrityError as ex:
ub.session.rollback() ub.session.rollback()
log.error("An unknown error occurred while saving scheduled tasks settings") log.error("An unknown error occurred while saving scheduled tasks settings")
@ -1869,3 +1876,13 @@ def extract_dynamic_field_from_filter(user, filtr):
def extract_user_identifier(user, filtr): def extract_user_identifier(user, filtr):
dynamic_field = extract_dynamic_field_from_filter(user, filtr) dynamic_field = extract_dynamic_field_from_filter(user, filtr)
return extract_user_data_from_field(user, dynamic_field) return extract_user_data_from_field(user, dynamic_field)
@admi.route("/ajax/canceltask", methods=['POST'])
@login_required
@admin_required
def cancel_task():
task_id = request.get_json().get('task_id', None)
worker = WorkerThread.get_instance()
worker.end_task(task_id)
return ""

@ -24,10 +24,13 @@ from sqlalchemy import __version__ as sql_version
sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0]) sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0])
# APP_MODE - production, development, or test
APP_MODE = os.environ.get('APP_MODE', 'production')
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present) # if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR')) HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
#In executables updater is not available, so variable is set to False there # In executables updater is not available, so variable is set to False there
UPDATER_AVAILABLE = True UPDATER_AVAILABLE = True
# Base dir is parent of current file, necessary if called from different folder # Base dir is parent of current file, necessary if called from different folder
@ -43,7 +46,7 @@ TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
# Cache dir - use CACHE_DIR environment variable, otherwise use the default directory: cps/cache # Cache dir - use CACHE_DIR environment variable, otherwise use the default directory: cps/cache
DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache') DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache')
CACHE_DIR = os.environ.get("CACHE_DIR", DEFAULT_CACHE_DIR) CACHE_DIR = os.environ.get('CACHE_DIR', DEFAULT_CACHE_DIR)
if HOME_CONFIG: if HOME_CONFIG:
home_dir = os.path.join(os.path.expanduser("~"),".calibre-web") home_dir = os.path.join(os.path.expanduser("~"),".calibre-web")

@ -443,11 +443,11 @@ class CalibreDB():
""" """
self.session = None self.session = None
if self._init: if self._init:
self.initSession(expire_on_commit) self.init_session(expire_on_commit)
self.instances.add(self) self.instances.add(self)
def initSession(self, expire_on_commit=True): def init_session(self, expire_on_commit=True):
self.session = self.session_factory() self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config) self.update_title_sort(self.config)
@ -593,7 +593,7 @@ class CalibreDB():
autoflush=True, autoflush=True,
bind=cls.engine)) bind=cls.engine))
for inst in cls.instances: for inst in cls.instances:
inst.initSession() inst.init_session()
cls._init = True cls._init = True
return True return True

@ -57,7 +57,8 @@ from . import logger, config, get_locale, db, fs, ub
from . import gdriveutils as gd from . import gdriveutils as gd
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES
from .subproc_wrapper import process_wait from .subproc_wrapper import process_wait
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
STAT_CANCELLED
from .tasks.mail import TaskEmail from .tasks.mail import TaskEmail
from .tasks.thumbnail import TaskClearCoverThumbnailCache from .tasks.thumbnail import TaskClearCoverThumbnailCache
@ -838,12 +839,22 @@ def render_task_status(tasklist):
ret['status'] = _(u'Started') ret['status'] = _(u'Started')
elif task.stat == STAT_FINISH_SUCCESS: elif task.stat == STAT_FINISH_SUCCESS:
ret['status'] = _(u'Finished') ret['status'] = _(u'Finished')
elif task.stat == STAT_ENDED:
ret['status'] = _(u'Ended')
elif task.stat == STAT_CANCELLED:
ret['status'] = _(u'Cancelled')
else: else:
ret['status'] = _(u'Unknown Status') ret['status'] = _(u'Unknown Status')
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message) ret['taskMessage'] = "{}: {}".format(_(task.name), task.message) if task.message else _(task.name)
ret['progress'] = "{} %".format(int(task.progress * 100)) ret['progress'] = "{} %".format(int(task.progress * 100))
ret['user'] = escape(user) # prevent xss ret['user'] = escape(user) # prevent xss
# Hidden fields
ret['id'] = task.id
ret['stat'] = task.stat
ret['is_cancellable'] = task.is_cancellable
renderedtasklist.append(ret) renderedtasklist.append(ret)
return renderedtasklist return renderedtasklist
@ -914,5 +925,5 @@ def get_download_link(book_id, book_format, client):
abort(404) abort(404)
def clear_cover_thumbnail_cache(book_id=None): def clear_cover_thumbnail_cache(book_id):
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id)) WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))

@ -17,29 +17,70 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
import datetime
from . import config, constants
from .services.background_scheduler import BackgroundScheduler from .services.background_scheduler import BackgroundScheduler
from .tasks.database import TaskReconnectDatabase from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails
from .services.worker import WorkerThread
def register_jobs(): def get_scheduled_tasks(reconnect=True):
tasks = list()
# Reconnect Calibre database (metadata.db)
if reconnect:
tasks.append(lambda: TaskReconnectDatabase())
# Generate all missing book cover thumbnails
if config.schedule_generate_book_covers:
tasks.append(lambda: TaskGenerateCoverThumbnails())
# Generate all missing series thumbnails
if config.schedule_generate_series_covers:
tasks.append(lambda: TaskGenerateSeriesThumbnails())
return tasks
def end_scheduled_tasks():
worker = WorkerThread.get_instance()
for __, __, __, task in worker.tasks:
if task.scheduled and task.is_cancellable:
worker.end_task(task.id)
def register_scheduled_tasks():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
if scheduler: if scheduler:
# Reconnect Calibre database (metadata.db) # Remove all existing jobs
scheduler.schedule_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16') scheduler.remove_all_jobs()
# Generate all missing book cover thumbnails start = config.schedule_start_time
scheduler.schedule_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4) end = config.schedule_end_time
# Generate all missing series thumbnails # Register scheduled tasks
scheduler.schedule_task(user=None, task=lambda: TaskGenerateSeriesThumbnails(), trigger='cron', hour=4) if start != end:
scheduler.schedule_tasks(tasks=get_scheduled_tasks(), trigger='cron', hour=start)
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', hour=end)
# Kick-off tasks, if they should currently be running
now = datetime.datetime.now().hour
if start <= now < end:
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
def register_startup_jobs():
def register_startup_tasks():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
if scheduler: if scheduler:
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateCoverThumbnails()) start = config.schedule_start_time
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateSeriesThumbnails()) end = config.schedule_end_time
now = datetime.datetime.now().hour
# Run scheduled tasks immediately for development and testing
# Ignore tasks that should currently be running, as these will be added when registering scheduled tasks
if constants.APP_MODE in ['development', 'test'] and not (start <= now < end):
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))

@ -48,23 +48,38 @@ class BackgroundScheduler:
return cls._instance return cls._instance
def _add(self, func, trigger, **trigger_args): def schedule(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)
# Expects a lambda expression for the task, so that the task isn't instantiated before the task is scheduled # Expects a lambda expression for the task
def schedule_task(self, user, task, trigger, **trigger_args): def schedule_task(self, task, user=None, trigger='cron', **trigger_args):
if use_APScheduler: if use_APScheduler:
def scheduled_task(): def scheduled_task():
worker_task = task() worker_task = task()
worker_task.scheduled = True
WorkerThread.add(user, worker_task) WorkerThread.add(user, worker_task)
return self.schedule(func=scheduled_task, trigger=trigger, **trigger_args)
return self._add(func=scheduled_task, trigger=trigger, **trigger_args) # Expects a list of lambda expressions for the tasks
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
if use_APScheduler:
for task in tasks:
self.schedule_task(task, user=user, trigger=trigger, **trigger_args)
# Expects a lambda expression for the task, so that the task isn't instantiated before the task is scheduled # Expects a lambda expression for the task
def schedule_task_immediately(self, user, task): def schedule_task_immediately(self, task, user=None):
if use_APScheduler: if use_APScheduler:
def scheduled_task(): def immediate_task():
WorkerThread.add(user, task()) WorkerThread.add(user, task())
return self.schedule(func=immediate_task, trigger='date')
# Expects a list of lambda expressions for the tasks
def schedule_tasks_immediately(self, tasks, user=None):
if use_APScheduler:
for task in tasks:
self.schedule_task_immediately(task, user)
return self._add(func=scheduled_task, trigger='date') # Remove all jobs
def remove_all_jobs(self):
self.scheduler.remove_all_jobs()

@ -21,6 +21,8 @@ STAT_WAITING = 0
STAT_FAIL = 1 STAT_FAIL = 1
STAT_STARTED = 2 STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3 STAT_FINISH_SUCCESS = 3
STAT_ENDED = 4
STAT_CANCELLED = 5
# Only retain this many tasks in dequeued list # Only retain this many tasks in dequeued list
TASK_CLEANUP_TRIGGER = 20 TASK_CLEANUP_TRIGGER = 20
@ -50,7 +52,7 @@ class WorkerThread(threading.Thread):
_instance = None _instance = None
@classmethod @classmethod
def getInstance(cls): def get_instance(cls):
if cls._instance is None: if cls._instance is None:
cls._instance = WorkerThread() cls._instance = WorkerThread()
return cls._instance return cls._instance
@ -67,12 +69,13 @@ class WorkerThread(threading.Thread):
@classmethod @classmethod
def add(cls, user, task): def add(cls, user, task):
ins = cls.getInstance() ins = cls.get_instance()
ins.num += 1 ins.num += 1
log.debug("Add Task for user: {}: {}".format(user, task)) username = user if user is not None else 'System'
log.debug("Add Task for user: {}: {}".format(username, task))
ins.queue.put(QueuedTask( ins.queue.put(QueuedTask(
num=ins.num, num=ins.num,
user=user, user=username,
added=datetime.now(), added=datetime.now(),
task=task, task=task,
)) ))
@ -134,6 +137,12 @@ class WorkerThread(threading.Thread):
self.queue.task_done() self.queue.task_done()
def end_task(self, task_id):
ins = self.get_instance()
for __, __, __, task in ins.tasks:
if str(task.id) == str(task_id) and task.is_cancellable:
task.stat = STAT_CANCELLED if task.stat == STAT_WAITING else STAT_ENDED
class CalibreTask: class CalibreTask:
__metaclass__ = abc.ABCMeta __metaclass__ = abc.ABCMeta
@ -147,10 +156,11 @@ class CalibreTask:
self.message = message self.message = message
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.self_cleanup = False self.self_cleanup = False
self._scheduled = False
@abc.abstractmethod @abc.abstractmethod
def run(self, worker_thread): def run(self, worker_thread):
"""Provides the caller some human-readable name for this class""" """The main entry-point for this task"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
@ -158,6 +168,11 @@ class CalibreTask:
"""Provides the caller some human-readable name for this class""" """Provides the caller some human-readable name for this class"""
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod
def is_cancellable(self):
"""Does this task gracefully handle being cancelled (STAT_ENDED, STAT_CANCELLED)?"""
raise NotImplementedError
def start(self, *args): def start(self, *args):
self.start_time = datetime.now() self.start_time = datetime.now()
self.stat = STAT_STARTED self.stat = STAT_STARTED
@ -208,7 +223,7 @@ class CalibreTask:
We have a separate dictating this because there may be certain tasks that want to override this We have a separate dictating this because there may be certain tasks that want to override this
""" """
# By default, we're good to clean a task if it's "Done" # By default, we're good to clean a task if it's "Done"
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL) return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL, STAT_ENDED, STAT_CANCELLED)
'''@progress.setter '''@progress.setter
def progress(self, x): def progress(self, x):
@ -226,6 +241,14 @@ class CalibreTask:
def self_cleanup(self, is_self_cleanup): def self_cleanup(self, is_self_cleanup):
self._self_cleanup = is_self_cleanup self._self_cleanup = is_self_cleanup
@property
def scheduled(self):
return self._scheduled
@scheduled.setter
def scheduled(self, is_scheduled):
self._scheduled = is_scheduled
def _handleError(self, error_message): def _handleError(self, error_message):
self.stat = STAT_FAIL self.stat = STAT_FAIL
self.progress = 1 self.progress = 1

@ -5150,7 +5150,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
pointer-events: none pointer-events: none
} }
#DeleteDomain:hover:before, #RestartDialog:hover:before, #ShutdownDialog:hover:before, #StatusDialog:hover:before, #ClearCacheDialog:hover:before, #deleteButton, #deleteModal:hover:before, body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover td > a:hover { #DeleteDomain:hover:before, #RestartDialog:hover:before, #ShutdownDialog:hover:before, #StatusDialog:hover:before, #deleteButton, #deleteModal:hover:before, #cancelTaskModal:hover:before, body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover td > a:hover {
cursor: pointer cursor: pointer
} }
@ -5237,6 +5237,10 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
margin-bottom: 20px margin-bottom: 20px
} }
body.admin > div.container-fluid div.scheduled_tasks_details {
margin-bottom: 20px
}
body.admin .btn-default { body.admin .btn-default {
margin-bottom: 10px margin-bottom: 10px
} }
@ -5468,7 +5472,7 @@ body.admin.modal-open .navbar {
z-index: 0 !important z-index: 0 !important
} }
#RestartDialog, #ShutdownDialog, #StatusDialog, #ClearCacheDialog, #deleteModal { #RestartDialog, #ShutdownDialog, #StatusDialog, #deleteModal, #cancelTaskModal {
top: 0; top: 0;
overflow: hidden; overflow: hidden;
padding-top: 70px; padding-top: 70px;
@ -5478,7 +5482,7 @@ body.admin.modal-open .navbar {
background: rgba(0, 0, 0, .5) background: rgba(0, 0, 0, .5)
} }
#RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #ClearCacheDialog:before, #deleteModal:before { #RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #deleteModal:before, #cancelTaskModal:before {
content: "\E208"; content: "\E208";
padding-right: 10px; padding-right: 10px;
display: block; display: block;
@ -5500,18 +5504,18 @@ body.admin.modal-open .navbar {
z-index: 99 z-index: 99
} }
#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before { #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #deleteModal.in:before, #cancelTaskModal.in:before {
-webkit-transform: translate(0, 0); -webkit-transform: translate(0, 0);
-ms-transform: translate(0, 0); -ms-transform: translate(0, 0);
transform: translate(0, 0) transform: translate(0, 0)
} }
#RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog { #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #deleteModal > .modal-dialog, #cancelTaskModal > .modal-dialog {
width: 450px; width: 450px;
margin: auto margin: auto
} }
#RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #ClearCacheDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content { #RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content, #cancelTaskModal > .modal-dialog > .modal-content {
max-height: calc(100% - 90px); max-height: calc(100% - 90px);
-webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
box-shadow: 0 5px 15px rgba(0, 0, 0, .5); box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
@ -5522,7 +5526,7 @@ body.admin.modal-open .navbar {
width: 450px width: 450px
} }
#RestartDialog > .modal-dialog > .modal-content > .modal-header, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header, #StatusDialog > .modal-dialog > .modal-content > .modal-header, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header, #deleteModal > .modal-dialog > .modal-content > .modal-header { #RestartDialog > .modal-dialog > .modal-content > .modal-header, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header, #StatusDialog > .modal-dialog > .modal-content > .modal-header, #deleteModal > .modal-dialog > .modal-content > .modal-header, #cancelTaskModal > .modal-dialog > .modal-content > .modal-header {
padding: 15px 20px; padding: 15px 20px;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
line-height: 1.71428571; line-height: 1.71428571;
@ -5535,7 +5539,7 @@ body.admin.modal-open .navbar {
text-align: left text-align: left
} }
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before, #StatusDialog > .modal-dialog > .modal-content > .modal-header:before, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before, #deleteModal > .modal-dialog > .modal-content > .modal-header:before { #RestartDialog > .modal-dialog > .modal-content > .modal-header:before, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before, #StatusDialog > .modal-dialog > .modal-content > .modal-header:before, #deleteModal > .modal-dialog > .modal-content > .modal-header:before, #cancelTaskModal > .modal-dialog > .modal-content > .modal-header:before {
padding-right: 10px; padding-right: 10px;
font-size: 18px; font-size: 18px;
color: #999; color: #999;
@ -5559,12 +5563,12 @@ body.admin.modal-open .navbar {
font-family: plex-icons-new, serif font-family: plex-icons-new, serif
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before { #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA15"; content: "\EA6D";
font-family: plex-icons-new, serif font-family: plex-icons-new, serif
} }
#deleteModal > .modal-dialog > .modal-content > .modal-header:before { #cancelTaskModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new, serif font-family: plex-icons-new, serif
} }
@ -5587,19 +5591,19 @@ body.admin.modal-open .navbar {
font-size: 20px font-size: 20px
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:after { #deleteModal > .modal-dialog > .modal-content > .modal-header:after {
content: "Clear Cover Thumbnail Cache"; content: "Delete Book";
display: inline-block; display: inline-block;
font-size: 20px font-size: 20px
} }
#deleteModal > .modal-dialog > .modal-content > .modal-header:after { #cancelTaskModal > .modal-dialog > .modal-content > .modal-header:after {
content: "Delete Book"; content: "Delete Book";
display: inline-block; display: inline-block;
font-size: 20px font-size: 20px
} }
#StatusDialog > .modal-dialog > .modal-content > .modal-header > span, #deleteModal > .modal-dialog > .modal-content > .modal-header > span, #loader > center > img, .rating-mobile { #StatusDialog > .modal-dialog > .modal-content > .modal-header > span, #deleteModal > .modal-dialog > .modal-content > .modal-header > span, #cancelTaskModal > .modal-dialog > .modal-content > .modal-header > span, #loader > center > img, .rating-mobile {
display: none display: none
} }
@ -5613,7 +5617,7 @@ body.admin.modal-open .navbar {
text-align: left text-align: left
} }
#ShutdownDialog > .modal-dialog > .modal-content > .modal-body, #StatusDialog > .modal-dialog > .modal-content > .modal-body, #deleteModal > .modal-dialog > .modal-content > .modal-body { #ShutdownDialog > .modal-dialog > .modal-content > .modal-body, #StatusDialog > .modal-dialog > .modal-content > .modal-body, #deleteModal > .modal-dialog > .modal-content > .modal-body, #cancelTaskModal > .modal-dialog > .modal-content > .modal-body {
padding: 20px 20px 40px; padding: 20px 20px 40px;
font-size: 16px; font-size: 16px;
line-height: 1.6em; line-height: 1.6em;
@ -5623,17 +5627,7 @@ body.admin.modal-open .navbar {
text-align: left text-align: left
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body { #RestartDialog > .modal-dialog > .modal-content > .modal-body > p, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > p, #StatusDialog > .modal-dialog > .modal-content > .modal-body > p, #deleteModal > .modal-dialog > .modal-content > .modal-body > p, #cancelTaskModal > .modal-dialog > .modal-content > .modal-body > p {
padding: 20px 20px 10px;
font-size: 16px;
line-height: 1.6em;
font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif;
color: #eee;
background: #282828;
text-align: left
}
#RestartDialog > .modal-dialog > .modal-content > .modal-body > p, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > p, #StatusDialog > .modal-dialog > .modal-content > .modal-body > p, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > p, #deleteModal > .modal-dialog > .modal-content > .modal-body > p {
padding: 20px 20px 0 0; padding: 20px 20px 0 0;
font-size: 16px; font-size: 16px;
line-height: 1.6em; line-height: 1.6em;
@ -5642,7 +5636,7 @@ body.admin.modal-open .navbar {
background: #282828 background: #282828
} }
#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart), #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown), #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache), #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default { #RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart), #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown), #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default, #cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
float: right; float: right;
z-index: 9; z-index: 9;
position: relative; position: relative;
@ -5678,11 +5672,11 @@ body.admin.modal-open .navbar {
border-radius: 3px border-radius: 3px
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > #clear_cache { #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
float: right; float: right;
z-index: 9; z-index: 9;
position: relative; position: relative;
margin: 25px 0 0 10px; margin: 0 0 0 10px;
min-width: 80px; min-width: 80px;
padding: 10px 18px; padding: 10px 18px;
font-size: 16px; font-size: 16px;
@ -5690,7 +5684,7 @@ body.admin.modal-open .navbar {
border-radius: 3px border-radius: 3px
} }
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger { #cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
float: right; float: right;
z-index: 9; z-index: 9;
position: relative; position: relative;
@ -5710,15 +5704,15 @@ body.admin.modal-open .navbar {
margin: 55px 0 0 10px margin: 55px 0 0 10px
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache) { #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
margin: 25px 0 0 10px margin: 0 0 0 10px
} }
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default { #cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
margin: 0 0 0 10px margin: 0 0 0 10px
} }
#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart):hover, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown):hover, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache):hover, #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover { #RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart):hover, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown):hover, #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover, #cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover {
background-color: hsla(0, 0%, 100%, .3) background-color: hsla(0, 0%, 100%, .3)
} }
@ -5752,21 +5746,6 @@ body.admin.modal-open .navbar {
box-shadow: 0 5px 15px rgba(0, 0, 0, .5) box-shadow: 0 5px 15px rgba(0, 0, 0, .5)
} }
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body:after {
content: '';
position: absolute;
width: 100%;
height: 72px;
background-color: #323232;
border-radius: 0 0 3px 3px;
left: 0;
margin-top: 10px;
z-index: 0;
border-top: 1px solid #222;
-webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
box-shadow: 0 5px 15px rgba(0, 0, 0, .5)
}
#deleteButton { #deleteButton {
position: fixed; position: fixed;
top: 60px; top: 60px;
@ -7355,11 +7334,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
background-color: transparent !important background-color: transparent !important
} }
#RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog { #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #deleteModal > .modal-dialog, #cancelTaskModal > .modal-dialog {
max-width: calc(100vw - 40px) max-width: calc(100vw - 40px)
} }
#RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #ClearCacheDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content { #RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content, #cancelTaskModal > .modal-dialog > .modal-content {
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
left: 0 left: 0
} }
@ -7509,7 +7488,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
padding: 30px 15px padding: 30px 15px
} }
#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before { #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #deleteModal.in:before, #cancelTaskModal.in:before {
left: auto; left: auto;
right: 34px right: 34px
} }

@ -454,18 +454,6 @@ $(function() {
} }
}); });
}); });
$("#clear_cache").click(function () {
$("#spinner3").show();
$.ajax({
dataType: "json",
url: window.location.pathname + "/../../clear-cache",
data: {"cache_type":"thumbnails"},
success: function(data) {
$("#spinner3").hide();
$("#ClearCacheDialog").modal("hide");
}
});
});
// Init all data control handlers to default // Init all data control handlers to default
$("input[data-control]").trigger("change"); $("input[data-control]").trigger("change");

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* exported TableActions, RestrictionActions, EbookActions, responseHandler */ /* exported TableActions, RestrictionActions, EbookActions, TaskActions, responseHandler */
/* global getPath, confirmDialog */ /* global getPath, confirmDialog */
var selections = []; var selections = [];
@ -42,6 +42,24 @@ $(function() {
}, 1000); }, 1000);
} }
$("#cancel_task_confirm").click(function() {
//get data-id attribute of the clicked element
var taskId = $(this).data("task-id");
$.ajax({
method: "post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../ajax/canceltask",
data: JSON.stringify({"task_id": taskId}),
});
});
//triggered when modal is about to be shown
$("#cancelTaskModal").on("show.bs.modal", function(e) {
//get data-id attribute of the clicked element and store in button
var taskId = $(e.relatedTarget).data("task-id");
$(e.currentTarget).find("#cancel_task_confirm").data("task-id", taskId);
});
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table", $("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) { function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter; var rows = rowsAfter;
@ -576,6 +594,7 @@ function handle_header_buttons () {
$(".header_select").removeAttr("disabled"); $(".header_select").removeAttr("disabled");
} }
} }
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function TableActions (value, row) { function TableActions (value, row) {
return [ return [
@ -613,6 +632,19 @@ function UserActions (value, row) {
].join(""); ].join("");
} }
/* Function for cancelling tasks */
function TaskActions (value, row) {
var cancellableStats = [0, 1, 2];
if (row.id && row.is_cancellable && cancellableStats.includes(row.stat)) {
return [
"<div class=\"task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.id + "\" title=\"Cancel\">",
"<i class=\"glyphicon glyphicon-ban-circle\"></i>",
"</div>"
].join("");
}
return '';
}
/* Function for keeping checked rows */ /* Function for keeping checked rows */
function responseHandler(res) { function responseHandler(res) {
$.each(res.rows, function (i, row) { $.each(res.rows, function (i, row) {

@ -234,3 +234,7 @@ class TaskConvert(CalibreTask):
@property @property
def name(self): def name(self):
return "Convert" return "Convert"
@property
def is_cancellable(self):
return False

@ -47,3 +47,7 @@ class TaskReconnectDatabase(CalibreTask):
@property @property
def name(self): def name(self):
return "Reconnect Database" return "Reconnect Database"
@property
def is_cancellable(self):
return False

@ -162,7 +162,6 @@ class TaskEmail(CalibreTask):
log.debug_or_exception(ex) log.debug_or_exception(ex)
self._handleError(u'Error sending e-mail: {}'.format(ex)) self._handleError(u'Error sending e-mail: {}'.format(ex))
def send_standard_email(self, msg): def send_standard_email(self, msg):
use_ssl = int(self.settings.get('mail_use_ssl', 0)) use_ssl = int(self.settings.get('mail_use_ssl', 0))
timeout = 600 # set timeout to 5mins timeout = 600 # set timeout to 5mins
@ -218,7 +217,6 @@ class TaskEmail(CalibreTask):
self.asyncSMTP = None self.asyncSMTP = None
self._progress = x self._progress = x
@classmethod @classmethod
def _get_attachment(cls, bookpath, filename): def _get_attachment(cls, bookpath, filename):
"""Get file as MIMEBase message""" """Get file as MIMEBase message"""
@ -260,5 +258,9 @@ class TaskEmail(CalibreTask):
def name(self): def name(self):
return "E-mail" return "E-mail"
@property
def is_cancellable(self):
return False
def __str__(self): def __str__(self):
return "{}, {}".format(self.name, self.subject) return "{}, {}".format(self.name, self.subject)

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) # This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 mmonkey # Copyright (C) 2020 monkey
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -21,8 +21,8 @@ import os
from .. import constants from .. import constants
from cps import config, db, fs, gdriveutils, logger, ub from cps import config, db, fs, gdriveutils, logger, ub
from cps.services.worker import CalibreTask from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
from datetime import datetime, timedelta from datetime import datetime
from sqlalchemy import func, text, or_ from sqlalchemy import func, text, or_
try: try:
@ -67,7 +67,7 @@ def get_best_fit(width, height, image_width, image_height):
class TaskGenerateCoverThumbnails(CalibreTask): class TaskGenerateCoverThumbnails(CalibreTask):
def __init__(self, task_message=u'Generating cover thumbnails'): def __init__(self, task_message=''):
super(TaskGenerateCoverThumbnails, self).__init__(task_message) super(TaskGenerateCoverThumbnails, self).__init__(task_message)
self.log = logger.create() self.log = logger.create()
self.app_db_session = ub.get_new_session_instance() self.app_db_session = ub.get_new_session_instance()
@ -79,13 +79,14 @@ class TaskGenerateCoverThumbnails(CalibreTask):
] ]
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 and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
self.message = 'Scanning Books'
books_with_covers = self.get_books_with_covers() books_with_covers = self.get_books_with_covers()
count = len(books_with_covers) count = len(books_with_covers)
updated = 0 total_generated = 0
generated = 0
for i, book in enumerate(books_with_covers): for i, book in enumerate(books_with_covers):
generated = 0
book_cover_thumbnails = self.get_book_cover_thumbnails(book.id) book_cover_thumbnails = self.get_book_cover_thumbnails(book.id)
# Generate new thumbnails for missing covers # Generate new thumbnails for missing covers
@ -98,16 +99,32 @@ class TaskGenerateCoverThumbnails(CalibreTask):
# Replace outdated or missing thumbnails # Replace outdated or missing thumbnails
for thumbnail in book_cover_thumbnails: for thumbnail in book_cover_thumbnails:
if book.last_modified > thumbnail.generated_at: if book.last_modified > thumbnail.generated_at:
updated += 1 generated += 1
self.update_book_cover_thumbnail(book, thumbnail) self.update_book_cover_thumbnail(book, thumbnail)
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS): elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
updated += 1 generated += 1
self.update_book_cover_thumbnail(book, thumbnail) self.update_book_cover_thumbnail(book, thumbnail)
self.message = u'Processing book {0} of {1}'.format(i + 1, count) # Increment the progress
self.progress = (1.0 / count) * i self.progress = (1.0 / count) * i
if generated > 0:
total_generated += generated
self.message = u'Generated {0} cover thumbnails'.format(total_generated)
# Check if job has been cancelled or ended
if self.stat == STAT_CANCELLED:
self.log.info(f'GenerateCoverThumbnails task has been cancelled.')
return
if self.stat == STAT_ENDED:
self.log.info(f'GenerateCoverThumbnails task has been ended.')
return
if total_generated == 0:
self.self_cleanup = True
self._handleSuccess() self._handleSuccess()
self.app_db_session.remove() self.app_db_session.remove()
@ -180,7 +197,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
self.log.info(u'Error generating thumbnail file: ' + str(ex)) self.log.info(u'Error generating thumbnail file: ' + str(ex))
raise ex raise ex
finally: finally:
stream.close() if stream is not None:
stream.close()
else: else:
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg')
if not os.path.isfile(book_cover_filepath): if not os.path.isfile(book_cover_filepath):
@ -197,11 +215,15 @@ class TaskGenerateCoverThumbnails(CalibreTask):
@property @property
def name(self): def name(self):
return "ThumbnailsGenerate" return 'GenerateCoverThumbnails'
@property
def is_cancellable(self):
return True
class TaskGenerateSeriesThumbnails(CalibreTask): class TaskGenerateSeriesThumbnails(CalibreTask):
def __init__(self, task_message=u'Generating series thumbnails'): def __init__(self, task_message=''):
super(TaskGenerateSeriesThumbnails, self).__init__(task_message) super(TaskGenerateSeriesThumbnails, self).__init__(task_message)
self.log = logger.create() self.log = logger.create()
self.app_db_session = ub.get_new_session_instance() self.app_db_session = ub.get_new_session_instance()
@ -209,17 +231,18 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
self.cache = fs.FileSystem() self.cache = fs.FileSystem()
self.resolutions = [ self.resolutions = [
constants.COVER_THUMBNAIL_SMALL, constants.COVER_THUMBNAIL_SMALL,
constants.COVER_THUMBNAIL_MEDIUM constants.COVER_THUMBNAIL_MEDIUM,
] ]
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 and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
self.message = 'Scanning Series'
all_series = self.get_series_with_four_plus_books() all_series = self.get_series_with_four_plus_books()
count = len(all_series) count = len(all_series)
updated = 0 total_generated = 0
generated = 0
for i, series in enumerate(all_series): for i, series in enumerate(all_series):
generated = 0
series_thumbnails = self.get_series_thumbnails(series.id) series_thumbnails = self.get_series_thumbnails(series.id)
series_books = self.get_series_books(series.id) series_books = self.get_series_books(series.id)
@ -233,16 +256,32 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
# Replace outdated or missing thumbnails # Replace outdated or missing thumbnails
for thumbnail in series_thumbnails: for thumbnail in series_thumbnails:
if any(book.last_modified > thumbnail.generated_at for book in series_books): if any(book.last_modified > thumbnail.generated_at for book in series_books):
updated += 1 generated += 1
self.update_series_thumbnail(series_books, thumbnail) self.update_series_thumbnail(series_books, thumbnail)
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS): elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
updated += 1 generated += 1
self.update_series_thumbnail(series_books, thumbnail) self.update_series_thumbnail(series_books, thumbnail)
self.message = u'Processing series {0} of {1}'.format(i + 1, count) # Increment the progress
self.progress = (1.0 / count) * i self.progress = (1.0 / count) * i
if generated > 0:
total_generated += generated
self.message = u'Generated {0} series thumbnails'.format(total_generated)
# Check if job has been cancelled or ended
if self.stat == STAT_CANCELLED:
self.log.info(f'GenerateSeriesThumbnails task has been cancelled.')
return
if self.stat == STAT_ENDED:
self.log.info(f'GenerateSeriesThumbnails task has been ended.')
return
if total_generated == 0:
self.self_cleanup = True
self._handleSuccess() self._handleSuccess()
self.app_db_session.remove() self.app_db_session.remove()
@ -302,7 +341,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
self.app_db_session.rollback() self.app_db_session.rollback()
def generate_series_thumbnail(self, series_books, thumbnail): def generate_series_thumbnail(self, series_books, thumbnail):
books = series_books[:4] # Get the last four books in the series based on series_index
books = sorted(series_books, key=lambda b: float(b.series_index), reverse=True)[:4]
top = 0 top = 0
left = 0 left = 0
@ -342,7 +382,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
self.log.info(u'Error generating thumbnail file: ' + str(ex)) self.log.info(u'Error generating thumbnail file: ' + str(ex))
raise ex raise ex
finally: finally:
stream.close() if stream is not None:
stream.close()
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg')
if not os.path.isfile(book_cover_filepath): if not os.path.isfile(book_cover_filepath):
@ -380,11 +421,15 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
@property @property
def name(self): def name(self):
return "SeriesThumbnailGenerate" return 'GenerateSeriesThumbnails'
@property
def is_cancellable(self):
return True
class TaskClearCoverThumbnailCache(CalibreTask): class TaskClearCoverThumbnailCache(CalibreTask):
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'): def __init__(self, book_id, task_message=u'Clearing cover thumbnail cache'):
super(TaskClearCoverThumbnailCache, self).__init__(task_message) super(TaskClearCoverThumbnailCache, self).__init__(task_message)
self.log = logger.create() self.log = logger.create()
self.book_id = book_id self.book_id = book_id
@ -397,8 +442,6 @@ class TaskClearCoverThumbnailCache(CalibreTask):
thumbnails = self.get_thumbnails_for_book(self.book_id) thumbnails = self.get_thumbnails_for_book(self.book_id)
for thumbnail in thumbnails: for thumbnail in thumbnails:
self.delete_thumbnail(thumbnail) self.delete_thumbnail(thumbnail)
else:
self.delete_all_thumbnails()
self._handleSuccess() self._handleSuccess()
self.app_db_session.remove() self.app_db_session.remove()
@ -411,19 +454,19 @@ class TaskClearCoverThumbnailCache(CalibreTask):
.all() .all()
def delete_thumbnail(self, thumbnail): def delete_thumbnail(self, thumbnail):
thumbnail.expiration = datetime.utcnow()
try: try:
self.app_db_session.commit()
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
except Exception as ex: except Exception as ex:
self.log.info(u'Error deleting book thumbnail: ' + str(ex)) self.log.info(u'Error deleting book thumbnail: ' + str(ex))
self._handleError(u'Error deleting book thumbnail: ' + str(ex)) self._handleError(u'Error deleting book thumbnail: ' + str(ex))
def delete_all_thumbnails(self):
try:
self.cache.delete_cache_dir(constants.CACHE_TYPE_THUMBNAILS)
except Exception as ex:
self.log.info(u'Error deleting book thumbnails: ' + str(ex))
self._handleError(u'Error deleting book thumbnails: ' + str(ex))
@property @property
def name(self): def name(self):
return "ThumbnailsClear" return 'ThumbnailsClear'
@property
def is_cancellable(self):
return False

@ -16,3 +16,7 @@ class TaskUpload(CalibreTask):
@property @property
def name(self): def name(self):
return "Upload" return "Upload"
@property
def is_cancellable(self):
return False

@ -159,7 +159,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2>{{_('Scheduled Tasks')}}</h2> <h2>{{_('Scheduled Tasks')}}</h2>
<div class="col-xs-12 col-sm-12"> <div class="col-xs-12 col-sm-12 scheduled_tasks_details">
<div class="row"> <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">{{_('Time at which tasks start to run')}}</div>
<div class="col-xs-6 col-sm-3">{{config.schedule_start_time}}:00</div> <div class="col-xs-6 col-sm-3">{{config.schedule_start_time}}:00</div>

@ -11,7 +11,7 @@
<label for="schedule_start_time">{{_('Time at which tasks start to run')}}</label> <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"> <select name="schedule_start_time" id="schedule_start_time" class="form-control">
{% for n in range(24) %} {% for n in range(24) %}
<option value="{{n}}" {% if content.schedule_start_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option> <option value="{{n}}" {% if config.schedule_start_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -19,12 +19,12 @@
<label for="schedule_end_time">{{_('Time at which tasks stop running')}}</label> <label for="schedule_end_time">{{_('Time at which tasks stop running')}}</label>
<select name="schedule_end_time" id="schedule_end_time" class="form-control"> <select name="schedule_end_time" id="schedule_end_time" class="form-control">
{% for n in range(24) %} {% for n in range(24) %}
<option value="{{n}}" {% if content.schedule_end_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option> <option value="{{n}}" {% if config.schedule_end_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="schedule_generate_book_covers" name="schedule_generate_book_covers" checked> <input type="checkbox" id="schedule_generate_book_covers" name="schedule_generate_book_covers" {% if config.schedule_generate_book_covers %}checked{% endif %}>
<label for="schedule_generate_book_covers">{{_('Generate Book Cover Thumbnails')}}</label> <label for="schedule_generate_book_covers">{{_('Generate Book Cover Thumbnails')}}</label>
</div> </div>
<div class="form-group"> <div class="form-group">

@ -16,6 +16,9 @@
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th> <th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th> <th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Run Time')}}</th>
<th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th> <th data-halign="right" data-align="right" data-field="starttime" data-sortable="true" data-sort-name="id">{{_('Start Time')}}</th>
{% if g.user.role_admin() %}
<th data-halign="right" data-align="right" data-formatter="TaskActions" data-switchable="false">{{_('Actions')}}</th>
{% endif %}
<th data-field="id" data-visible="false"></th> <th data-field="id" data-visible="false"></th>
<th data-field="rt" data-visible="false"></th> <th data-field="rt" data-visible="false"></th>
</tr> </tr>
@ -23,6 +26,30 @@
</table> </table>
</div> </div>
{% endblock %} {% endblock %}
{% block modal %}
{{ delete_book() }}
{% if g.user.role_admin() %}
<div class="modal fade" id="cancelTaskModal" role="dialog" aria-labelledby="metaCancelTaskLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body text-center">
<p>
<span>{{_('This task will be cancelled. Any progress made by this task will be saved.')}}</span>
<span>{{_('If this is a scheduled task, it will be re-ran during the next scheduled time.')}}</span>
</p>
</div>
<div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Ok')}}" name="cancel_task_confirm" id="cancel_task_confirm" data-dismiss="modal">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block js %} {% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script> <script src="{{ url_for('static', filename='js/table.js') }}"></script>

@ -124,7 +124,7 @@ def viewer_required(f):
@web.route("/ajax/emailstat") @web.route("/ajax/emailstat")
@login_required @login_required
def get_email_status_json(): def get_email_status_json():
tasks = WorkerThread.getInstance().tasks tasks = WorkerThread.get_instance().tasks
return jsonify(render_task_status(tasks)) return jsonify(render_task_status(tasks))
@ -1055,7 +1055,7 @@ def category_list():
@login_required @login_required
def get_tasks_status(): def get_tasks_status():
# if current user admin, show all email, otherwise only own emails # if current user admin, show all email, otherwise only own emails
tasks = WorkerThread.getInstance().tasks tasks = WorkerThread.get_instance().tasks
answer = render_task_status(tasks) answer = render_task_status(tasks)
return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks") return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")

Loading…
Cancel
Save