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.
This commit is contained in:
parent
26071d4e7a
commit
46205a1f83
8
cps.py
8
cps.py
|
@ -44,7 +44,7 @@ from cps.editbooks import editbook
|
|||
from cps.remotelogin import remotelogin
|
||||
from cps.search_metadata import meta
|
||||
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:
|
||||
from cps.kobo import kobo, get_kobo_activated
|
||||
|
@ -81,9 +81,9 @@ def main():
|
|||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
|
||||
# Register scheduled jobs
|
||||
register_jobs()
|
||||
# register_startup_jobs()
|
||||
# Register scheduled tasks
|
||||
register_scheduled_tasks()
|
||||
register_startup_tasks()
|
||||
|
||||
success = web_server.start()
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
23
cps/admin.py
23
cps/admin.py
|
@ -40,12 +40,13 @@ from sqlalchemy.orm.attributes import flag_modified
|
|||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import func, or_, text
|
||||
|
||||
from . import constants, logger, helper, services, isoLanguages, fs
|
||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
||||
from . import constants, logger, helper, services, isoLanguages
|
||||
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, \
|
||||
valid_email, check_username
|
||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||
from .render_template import render_title_template, get_sidebar_config
|
||||
from .services.worker import WorkerThread
|
||||
from . import debug_info, _BABEL_TRANSLATIONS
|
||||
|
||||
try:
|
||||
|
@ -1568,7 +1569,7 @@ def update_mailsettings():
|
|||
@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"))
|
||||
return render_title_template("schedule_edit.html", config=content, title=_(u"Edit Scheduled Tasks Settings"))
|
||||
|
||||
|
||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||
|
@ -1584,6 +1585,12 @@ def update_scheduledtasks():
|
|||
try:
|
||||
config.save()
|
||||
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:
|
||||
ub.session.rollback()
|
||||
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):
|
||||
dynamic_field = extract_dynamic_field_from_filter(user, filtr)
|
||||
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,6 +24,9 @@ from sqlalchemy import __version__ as sql_version
|
|||
|
||||
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)
|
||||
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
|
||||
|
||||
|
@ -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
|
||||
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:
|
||||
home_dir = os.path.join(os.path.expanduser("~"),".calibre-web")
|
||||
|
|
|
@ -443,11 +443,11 @@ class CalibreDB():
|
|||
"""
|
||||
self.session = None
|
||||
if self._init:
|
||||
self.initSession(expire_on_commit)
|
||||
self.init_session(expire_on_commit)
|
||||
|
||||
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.expire_on_commit = expire_on_commit
|
||||
self.update_title_sort(self.config)
|
||||
|
@ -593,7 +593,7 @@ class CalibreDB():
|
|||
autoflush=True,
|
||||
bind=cls.engine))
|
||||
for inst in cls.instances:
|
||||
inst.initSession()
|
||||
inst.init_session()
|
||||
|
||||
cls._init = True
|
||||
return True
|
||||
|
|
|
@ -57,7 +57,8 @@ from . import logger, config, get_locale, db, fs, ub
|
|||
from . import gdriveutils as gd
|
||||
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES
|
||||
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.thumbnail import TaskClearCoverThumbnailCache
|
||||
|
||||
|
@ -838,12 +839,22 @@ def render_task_status(tasklist):
|
|||
ret['status'] = _(u'Started')
|
||||
elif task.stat == STAT_FINISH_SUCCESS:
|
||||
ret['status'] = _(u'Finished')
|
||||
elif task.stat == STAT_ENDED:
|
||||
ret['status'] = _(u'Ended')
|
||||
elif task.stat == STAT_CANCELLED:
|
||||
ret['status'] = _(u'Cancelled')
|
||||
else:
|
||||
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['user'] = escape(user) # prevent xss
|
||||
|
||||
# Hidden fields
|
||||
ret['id'] = task.id
|
||||
ret['stat'] = task.stat
|
||||
ret['is_cancellable'] = task.is_cancellable
|
||||
|
||||
renderedtasklist.append(ret)
|
||||
|
||||
return renderedtasklist
|
||||
|
@ -914,5 +925,5 @@ def get_download_link(book_id, book_format, client):
|
|||
abort(404)
|
||||
|
||||
|
||||
def clear_cover_thumbnail_cache(book_id=None):
|
||||
def clear_cover_thumbnail_cache(book_id):
|
||||
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))
|
||||
|
|
|
@ -17,29 +17,70 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import datetime
|
||||
|
||||
from . import config, constants
|
||||
from .services.background_scheduler import BackgroundScheduler
|
||||
from .tasks.database import TaskReconnectDatabase
|
||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails
|
||||
from .services.worker import WorkerThread
|
||||
|
||||
|
||||
def register_jobs():
|
||||
scheduler = BackgroundScheduler()
|
||||
def get_scheduled_tasks(reconnect=True):
|
||||
tasks = list()
|
||||
|
||||
if scheduler:
|
||||
# Reconnect Calibre database (metadata.db)
|
||||
scheduler.schedule_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16')
|
||||
if reconnect:
|
||||
tasks.append(lambda: TaskReconnectDatabase())
|
||||
|
||||
# Generate all missing book cover thumbnails
|
||||
scheduler.schedule_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4)
|
||||
if config.schedule_generate_book_covers:
|
||||
tasks.append(lambda: TaskGenerateCoverThumbnails())
|
||||
|
||||
# Generate all missing series thumbnails
|
||||
scheduler.schedule_task(user=None, task=lambda: TaskGenerateSeriesThumbnails(), trigger='cron', hour=4)
|
||||
if config.schedule_generate_series_covers:
|
||||
tasks.append(lambda: TaskGenerateSeriesThumbnails())
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def register_startup_jobs():
|
||||
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()
|
||||
|
||||
if scheduler:
|
||||
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateCoverThumbnails())
|
||||
scheduler.schedule_task_immediately(None, task=lambda: TaskGenerateSeriesThumbnails())
|
||||
# Remove all existing jobs
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
start = config.schedule_start_time
|
||||
end = config.schedule_end_time
|
||||
|
||||
# Register scheduled tasks
|
||||
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_tasks():
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
if scheduler:
|
||||
start = config.schedule_start_time
|
||||
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
|
||||
|
||||
def _add(self, func, trigger, **trigger_args):
|
||||
def schedule(self, func, trigger, **trigger_args):
|
||||
if use_APScheduler:
|
||||
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
|
||||
def schedule_task(self, user, task, trigger, **trigger_args):
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task(self, task, user=None, trigger='cron', **trigger_args):
|
||||
if use_APScheduler:
|
||||
def scheduled_task():
|
||||
worker_task = task()
|
||||
worker_task.scheduled = True
|
||||
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 lambda expression for the task, so that the task isn't instantiated before the task is scheduled
|
||||
def schedule_task_immediately(self, user, task):
|
||||
# Expects a list of lambda expressions for the tasks
|
||||
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
|
||||
if use_APScheduler:
|
||||
def scheduled_task():
|
||||
WorkerThread.add(user, task())
|
||||
for task in tasks:
|
||||
self.schedule_task(task, user=user, trigger=trigger, **trigger_args)
|
||||
|
||||
return self._add(func=scheduled_task, trigger='date')
|
||||
# Expects a lambda expression for the task
|
||||
def schedule_task_immediately(self, task, user=None):
|
||||
if use_APScheduler:
|
||||
def immediate_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)
|
||||
|
||||
# Remove all jobs
|
||||
def remove_all_jobs(self):
|
||||
self.scheduler.remove_all_jobs()
|
||||
|
|
|
@ -21,6 +21,8 @@ STAT_WAITING = 0
|
|||
STAT_FAIL = 1
|
||||
STAT_STARTED = 2
|
||||
STAT_FINISH_SUCCESS = 3
|
||||
STAT_ENDED = 4
|
||||
STAT_CANCELLED = 5
|
||||
|
||||
# Only retain this many tasks in dequeued list
|
||||
TASK_CLEANUP_TRIGGER = 20
|
||||
|
@ -50,7 +52,7 @@ class WorkerThread(threading.Thread):
|
|||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = WorkerThread()
|
||||
return cls._instance
|
||||
|
@ -67,12 +69,13 @@ class WorkerThread(threading.Thread):
|
|||
|
||||
@classmethod
|
||||
def add(cls, user, task):
|
||||
ins = cls.getInstance()
|
||||
ins = cls.get_instance()
|
||||
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(
|
||||
num=ins.num,
|
||||
user=user,
|
||||
user=username,
|
||||
added=datetime.now(),
|
||||
task=task,
|
||||
))
|
||||
|
@ -134,6 +137,12 @@ class WorkerThread(threading.Thread):
|
|||
|
||||
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:
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
@ -147,10 +156,11 @@ class CalibreTask:
|
|||
self.message = message
|
||||
self.id = uuid.uuid4()
|
||||
self.self_cleanup = False
|
||||
self._scheduled = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self, worker_thread):
|
||||
"""Provides the caller some human-readable name for this class"""
|
||||
"""The main entry-point for this task"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -158,6 +168,11 @@ class CalibreTask:
|
|||
"""Provides the caller some human-readable name for this class"""
|
||||
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):
|
||||
self.start_time = datetime.now()
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
def progress(self, x):
|
||||
|
@ -226,6 +241,14 @@ class CalibreTask:
|
|||
def self_cleanup(self, 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):
|
||||
self.stat = STAT_FAIL
|
||||
self.progress = 1
|
||||
|
|
|
@ -5150,7 +5150,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -5237,6 +5237,10 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
|
|||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
body.admin > div.container-fluid div.scheduled_tasks_details {
|
||||
margin-bottom: 20px
|
||||
}
|
||||
|
||||
body.admin .btn-default {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
@ -5468,7 +5472,7 @@ body.admin.modal-open .navbar {
|
|||
z-index: 0 !important
|
||||
}
|
||||
|
||||
#RestartDialog, #ShutdownDialog, #StatusDialog, #ClearCacheDialog, #deleteModal {
|
||||
#RestartDialog, #ShutdownDialog, #StatusDialog, #deleteModal, #cancelTaskModal {
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 70px;
|
||||
|
@ -5478,7 +5482,7 @@ body.admin.modal-open .navbar {
|
|||
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";
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
|
@ -5500,18 +5504,18 @@ body.admin.modal-open .navbar {
|
|||
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);
|
||||
-ms-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;
|
||||
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);
|
||||
-webkit-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
|
||||
}
|
||||
|
||||
#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;
|
||||
border-radius: 3px 3px 0 0;
|
||||
line-height: 1.71428571;
|
||||
|
@ -5535,7 +5539,7 @@ body.admin.modal-open .navbar {
|
|||
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;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
|
@ -5559,12 +5563,12 @@ body.admin.modal-open .navbar {
|
|||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA15";
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
|
||||
#cancelTaskModal > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
@ -5587,19 +5591,19 @@ body.admin.modal-open .navbar {
|
|||
font-size: 20px
|
||||
}
|
||||
|
||||
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:after {
|
||||
content: "Clear Cover Thumbnail Cache";
|
||||
display: inline-block;
|
||||
font-size: 20px
|
||||
}
|
||||
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:after {
|
||||
content: "Delete Book";
|
||||
display: inline-block;
|
||||
font-size: 20px
|
||||
}
|
||||
|
||||
#StatusDialog > .modal-dialog > .modal-content > .modal-header > span, #deleteModal > .modal-dialog > .modal-content > .modal-header > span, #loader > center > img, .rating-mobile {
|
||||
#cancelTaskModal > .modal-dialog > .modal-content > .modal-header:after {
|
||||
content: "Delete Book";
|
||||
display: inline-block;
|
||||
font-size: 20px
|
||||
}
|
||||
|
||||
#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
|
||||
}
|
||||
|
||||
|
@ -5613,7 +5617,7 @@ body.admin.modal-open .navbar {
|
|||
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;
|
||||
font-size: 16px;
|
||||
line-height: 1.6em;
|
||||
|
@ -5623,17 +5627,7 @@ body.admin.modal-open .navbar {
|
|||
text-align: left
|
||||
}
|
||||
|
||||
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body {
|
||||
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 {
|
||||
#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 0 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.6em;
|
||||
|
@ -5642,7 +5636,7 @@ body.admin.modal-open .navbar {
|
|||
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;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
|
@ -5678,11 +5672,11 @@ body.admin.modal-open .navbar {
|
|||
border-radius: 3px
|
||||
}
|
||||
|
||||
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > #clear_cache {
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
|
||||
float: right;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
margin: 25px 0 0 10px;
|
||||
margin: 0 0 0 10px;
|
||||
min-width: 80px;
|
||||
padding: 10px 18px;
|
||||
font-size: 16px;
|
||||
|
@ -5690,7 +5684,7 @@ body.admin.modal-open .navbar {
|
|||
border-radius: 3px
|
||||
}
|
||||
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
|
||||
#cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
|
||||
float: right;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
|
@ -5710,15 +5704,15 @@ body.admin.modal-open .navbar {
|
|||
margin: 55px 0 0 10px
|
||||
}
|
||||
|
||||
#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache) {
|
||||
margin: 25px 0 0 10px
|
||||
}
|
||||
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
|
||||
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 {
|
||||
#cancelTaskModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
|
||||
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, #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)
|
||||
}
|
||||
|
||||
|
@ -5752,21 +5746,6 @@ body.admin.modal-open .navbar {
|
|||
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 {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
|
@ -7355,11 +7334,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||
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)
|
||||
}
|
||||
|
||||
#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);
|
||||
left: 0
|
||||
}
|
||||
|
@ -7509,7 +7488,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||
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;
|
||||
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
|
||||
$("input[data-control]").trigger("change");
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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 */
|
||||
|
||||
var selections = [];
|
||||
|
@ -42,6 +42,24 @@ $(function() {
|
|||
}, 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",
|
||||
function (e, rowsAfter, rowsBefore) {
|
||||
var rows = rowsAfter;
|
||||
|
@ -576,6 +594,7 @@ function handle_header_buttons () {
|
|||
$(".header_select").removeAttr("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/* Function for deleting domain restrictions */
|
||||
function TableActions (value, row) {
|
||||
return [
|
||||
|
@ -613,6 +632,19 @@ function UserActions (value, row) {
|
|||
].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 responseHandler(res) {
|
||||
$.each(res.rows, function (i, row) {
|
||||
|
|
|
@ -234,3 +234,7 @@ class TaskConvert(CalibreTask):
|
|||
@property
|
||||
def name(self):
|
||||
return "Convert"
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return False
|
||||
|
|
|
@ -47,3 +47,7 @@ class TaskReconnectDatabase(CalibreTask):
|
|||
@property
|
||||
def name(self):
|
||||
return "Reconnect Database"
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return False
|
||||
|
|
|
@ -162,7 +162,6 @@ class TaskEmail(CalibreTask):
|
|||
log.debug_or_exception(ex)
|
||||
self._handleError(u'Error sending e-mail: {}'.format(ex))
|
||||
|
||||
|
||||
def send_standard_email(self, msg):
|
||||
use_ssl = int(self.settings.get('mail_use_ssl', 0))
|
||||
timeout = 600 # set timeout to 5mins
|
||||
|
@ -218,7 +217,6 @@ class TaskEmail(CalibreTask):
|
|||
self.asyncSMTP = None
|
||||
self._progress = x
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_attachment(cls, bookpath, filename):
|
||||
"""Get file as MIMEBase message"""
|
||||
|
@ -260,5 +258,9 @@ class TaskEmail(CalibreTask):
|
|||
def name(self):
|
||||
return "E-mail"
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "{}, {}".format(self.name, self.subject)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -21,8 +21,8 @@ import os
|
|||
|
||||
from .. import constants
|
||||
from cps import config, db, fs, gdriveutils, logger, ub
|
||||
from cps.services.worker import CalibreTask
|
||||
from datetime import datetime, timedelta
|
||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func, text, or_
|
||||
|
||||
try:
|
||||
|
@ -67,7 +67,7 @@ def get_best_fit(width, height, image_width, image_height):
|
|||
|
||||
|
||||
class TaskGenerateCoverThumbnails(CalibreTask):
|
||||
def __init__(self, task_message=u'Generating cover thumbnails'):
|
||||
def __init__(self, task_message=''):
|
||||
super(TaskGenerateCoverThumbnails, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.app_db_session = ub.get_new_session_instance()
|
||||
|
@ -79,13 +79,14 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||
]
|
||||
|
||||
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()
|
||||
count = len(books_with_covers)
|
||||
|
||||
updated = 0
|
||||
generated = 0
|
||||
total_generated = 0
|
||||
for i, book in enumerate(books_with_covers):
|
||||
generated = 0
|
||||
book_cover_thumbnails = self.get_book_cover_thumbnails(book.id)
|
||||
|
||||
# Generate new thumbnails for missing covers
|
||||
|
@ -98,16 +99,32 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||
# Replace outdated or missing thumbnails
|
||||
for thumbnail in book_cover_thumbnails:
|
||||
if book.last_modified > thumbnail.generated_at:
|
||||
updated += 1
|
||||
generated += 1
|
||||
self.update_book_cover_thumbnail(book, thumbnail)
|
||||
|
||||
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.message = u'Processing book {0} of {1}'.format(i + 1, count)
|
||||
# Increment the progress
|
||||
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.app_db_session.remove()
|
||||
|
||||
|
@ -180,6 +197,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||
self.log.info(u'Error generating thumbnail file: ' + str(ex))
|
||||
raise ex
|
||||
finally:
|
||||
if stream is not None:
|
||||
stream.close()
|
||||
else:
|
||||
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg')
|
||||
|
@ -197,11 +215,15 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||
|
||||
@property
|
||||
def name(self):
|
||||
return "ThumbnailsGenerate"
|
||||
return 'GenerateCoverThumbnails'
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return True
|
||||
|
||||
|
||||
class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
def __init__(self, task_message=u'Generating series thumbnails'):
|
||||
def __init__(self, task_message=''):
|
||||
super(TaskGenerateSeriesThumbnails, self).__init__(task_message)
|
||||
self.log = logger.create()
|
||||
self.app_db_session = ub.get_new_session_instance()
|
||||
|
@ -209,17 +231,18 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||
self.cache = fs.FileSystem()
|
||||
self.resolutions = [
|
||||
constants.COVER_THUMBNAIL_SMALL,
|
||||
constants.COVER_THUMBNAIL_MEDIUM
|
||||
constants.COVER_THUMBNAIL_MEDIUM,
|
||||
]
|
||||
|
||||
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()
|
||||
count = len(all_series)
|
||||
|
||||
updated = 0
|
||||
generated = 0
|
||||
total_generated = 0
|
||||
for i, series in enumerate(all_series):
|
||||
generated = 0
|
||||
series_thumbnails = self.get_series_thumbnails(series.id)
|
||||
series_books = self.get_series_books(series.id)
|
||||
|
||||
|
@ -233,16 +256,32 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||
# Replace outdated or missing thumbnails
|
||||
for thumbnail in series_thumbnails:
|
||||
if any(book.last_modified > thumbnail.generated_at for book in series_books):
|
||||
updated += 1
|
||||
generated += 1
|
||||
self.update_series_thumbnail(series_books, thumbnail)
|
||||
|
||||
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.message = u'Processing series {0} of {1}'.format(i + 1, count)
|
||||
# Increment the progress
|
||||
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.app_db_session.remove()
|
||||
|
||||
|
@ -302,7 +341,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||
self.app_db_session.rollback()
|
||||
|
||||
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
|
||||
left = 0
|
||||
|
@ -342,6 +382,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||
self.log.info(u'Error generating thumbnail file: ' + str(ex))
|
||||
raise ex
|
||||
finally:
|
||||
if stream is not None:
|
||||
stream.close()
|
||||
|
||||
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg')
|
||||
|
@ -380,11 +421,15 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||
|
||||
@property
|
||||
def name(self):
|
||||
return "SeriesThumbnailGenerate"
|
||||
return 'GenerateSeriesThumbnails'
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
self.log = logger.create()
|
||||
self.book_id = book_id
|
||||
|
@ -397,8 +442,6 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
|||
thumbnails = self.get_thumbnails_for_book(self.book_id)
|
||||
for thumbnail in thumbnails:
|
||||
self.delete_thumbnail(thumbnail)
|
||||
else:
|
||||
self.delete_all_thumbnails()
|
||||
|
||||
self._handleSuccess()
|
||||
self.app_db_session.remove()
|
||||
|
@ -411,19 +454,19 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
|||
.all()
|
||||
|
||||
def delete_thumbnail(self, thumbnail):
|
||||
thumbnail.expiration = datetime.utcnow()
|
||||
|
||||
try:
|
||||
self.app_db_session.commit()
|
||||
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
||||
except Exception as ex:
|
||||
self.log.info(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
|
||||
def name(self):
|
||||
return "ThumbnailsClear"
|
||||
return 'ThumbnailsClear'
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return False
|
||||
|
|
|
@ -16,3 +16,7 @@ class TaskUpload(CalibreTask):
|
|||
@property
|
||||
def name(self):
|
||||
return "Upload"
|
||||
|
||||
@property
|
||||
def is_cancellable(self):
|
||||
return False
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<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="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>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<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>
|
||||
<option value="{{n}}" {% if config.schedule_start_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -19,12 +19,12 @@
|
|||
<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>
|
||||
<option value="{{n}}" {% if config.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>
|
||||
<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>
|
||||
</div>
|
||||
<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="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>
|
||||
{% 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="rt" data-visible="false"></th>
|
||||
</tr>
|
||||
|
@ -23,6 +26,30 @@
|
|||
</table>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
|
|
@ -124,7 +124,7 @@ def viewer_required(f):
|
|||
@web.route("/ajax/emailstat")
|
||||
@login_required
|
||||
def get_email_status_json():
|
||||
tasks = WorkerThread.getInstance().tasks
|
||||
tasks = WorkerThread.get_instance().tasks
|
||||
return jsonify(render_task_status(tasks))
|
||||
|
||||
|
||||
|
@ -1055,7 +1055,7 @@ def category_list():
|
|||
@login_required
|
||||
def get_tasks_status():
|
||||
# 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)
|
||||
return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user