diff --git a/cps/admin.py b/cps/admin.py
index 8b3ca247..dbc1b708 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -38,7 +38,7 @@ from sqlalchemy import and_
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_
-from . import constants, logger, helper, services
+from . import constants, logger, helper, services, fs
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
from .gdriveutils import is_gdrive_ready, gdrive_support
@@ -157,6 +157,23 @@ def shutdown():
return json.dumps(showtext), 400
+@admi.route("/clear-cache")
+@login_required
+@admin_required
+def clear_cache():
+ cache_type = request.args.get('cache_type'.strip())
+ showtext = {}
+
+ if cache_type == fs.CACHE_TYPE_THUMBNAILS:
+ log.info('clearing cover thumbnail cache')
+ showtext['text'] = _(u'Cleared cover thumbnail cache')
+ helper.clear_cover_thumbnail_cache()
+ return json.dumps(showtext)
+
+ showtext['text'] = _(u'Unknown command')
+ return json.dumps(showtext)
+
+
@admi.route("/admin/view")
@login_required
@admin_required
diff --git a/cps/editbooks.py b/cps/editbooks.py
index 08ee93b1..6d26ebca 100644
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -595,6 +595,7 @@ def upload_cover(request, book):
abort(403)
ret, message = helper.save_cover(requested_file, book.path)
if ret is True:
+ helper.clear_cover_thumbnail_cache(book.id)
return True
else:
flash(message, category="error")
@@ -684,6 +685,7 @@ def edit_book(book_id):
if result is True:
book.has_cover = 1
modif_date = True
+ helper.clear_cover_thumbnail_cache(book.id)
else:
flash(error, category="error")
diff --git a/cps/helper.py b/cps/helper.py
index 271ab3e9..0b0c675f 100644
--- a/cps/helper.py
+++ b/cps/helper.py
@@ -58,6 +58,7 @@ from .constants import STATIC_DIR as _STATIC_DIR
from .subproc_wrapper import process_wait
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
from .tasks.mail import TaskEmail
+from .tasks.thumbnail import TaskClearCoverThumbnailCache
log = logger.create()
@@ -525,6 +526,7 @@ def update_dir_stucture(book_id, calibrepath, first_author=None, orignal_filepat
def delete_book(book, calibrepath, book_format):
+ clear_cover_thumbnail_cache(book.id)
if config.config_use_google_drive:
return delete_book_gdrive(book, book_format)
else:
@@ -538,9 +540,9 @@ def get_cover_on_failure(use_generic_cover):
return None
-def get_book_cover(book_id, resolution=1):
+def get_book_cover(book_id):
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
- return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
+ return get_book_cover_internal(book, use_generic_cover_on_failure=True)
def get_book_cover_with_uuid(book_uuid, use_generic_cover_on_failure=True):
@@ -548,11 +550,19 @@ def get_book_cover_with_uuid(book_uuid, use_generic_cover_on_failure=True):
return get_book_cover_internal(book, use_generic_cover_on_failure)
-def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=1, disable_thumbnail=False):
+def get_cached_book_cover(cache_id):
+ parts = cache_id.split('_')
+ book_uuid = parts[0] if len(parts) else None
+ resolution = parts[2] if len(parts) > 2 else None
+ book = calibre_db.get_book_by_uuid(book_uuid) if book_uuid else None
+ return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
+
+
+def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None):
if book and book.has_cover:
# Send the book cover thumbnail if it exists in cache
- if not disable_thumbnail:
+ if resolution:
thumbnail = get_book_cover_thumbnail(book, resolution)
if thumbnail:
cache = fs.FileSystem()
@@ -585,7 +595,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=1, di
return get_cover_on_failure(use_generic_cover_on_failure)
-def get_book_cover_thumbnail(book, resolution=1):
+def get_book_cover_thumbnail(book, resolution):
if book and book.has_cover:
return ub.session\
.query(ub.Thumbnail)\
@@ -846,3 +856,6 @@ def get_download_link(book_id, book_format, client):
else:
abort(404)
+
+def clear_cover_thumbnail_cache(book_id=None):
+ WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))
diff --git a/cps/jinjia.py b/cps/jinjia.py
index 688d1fba..bf81c059 100644
--- a/cps/jinjia.py
+++ b/cps/jinjia.py
@@ -128,8 +128,14 @@ def formatseriesindex_filter(series_index):
return series_index
return 0
+
@jinjia.app_template_filter('uuidfilter')
def uuidfilter(var):
return uuid4()
+@jinjia.app_template_filter('book_cover_cache_id')
+def book_cover_cache_id(book, resolution=None):
+ timestamp = int(book.last_modified.timestamp() * 1000)
+ cache_bust = str(book.uuid) + '_' + str(timestamp)
+ return cache_bust if not resolution else cache_bust + '_' + str(resolution)
diff --git a/cps/schedule.py b/cps/schedule.py
index 5c658e41..f349a231 100644
--- a/cps/schedule.py
+++ b/cps/schedule.py
@@ -18,12 +18,10 @@
from __future__ import division, print_function, unicode_literals
-from . import config, db, logger, ub
from .services.background_scheduler import BackgroundScheduler
+from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskCleanupCoverThumbnailCache, TaskGenerateCoverThumbnails
-log = logger.create()
-
def register_jobs():
scheduler = BackgroundScheduler()
@@ -35,10 +33,4 @@ def register_jobs():
scheduler.add_task(user=None, task=lambda: TaskCleanupCoverThumbnailCache(), trigger='cron', hour=4)
# Reconnect metadata.db every 4 hours
- scheduler.add(func=reconnect_db_job, trigger='interval', hours=4)
-
-
-def reconnect_db_job():
- log.info('Running background task: reconnect to calibre database')
- calibre_db = db.CalibreDB()
- calibre_db.reconnect_db(config, ub.app_DB_path)
+ scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='interval', hours=4)
diff --git a/cps/static/css/caliBlur.css b/cps/static/css/caliBlur.css
index d085608d..da5d2933 100644
--- a/cps/static/css/caliBlur.css
+++ b/cps/static/css/caliBlur.css
@@ -5167,7 +5167,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, #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, #ClearCacheDialog:hover:before, #deleteButton, #deleteModal:hover:before, body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover td > a:hover {
cursor: pointer
}
@@ -5254,7 +5254,7 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
margin-bottom: 20px
}
-body.admin:not(.modal-open) .btn-default {
+body.admin .btn-default {
margin-bottom: 10px
}
@@ -5485,7 +5485,7 @@ body.admin.modal-open .navbar {
z-index: 0 !important
}
-#RestartDialog, #ShutdownDialog, #StatusDialog, #deleteModal {
+#RestartDialog, #ShutdownDialog, #StatusDialog, #ClearCacheDialog, #deleteModal {
top: 0;
overflow: hidden;
padding-top: 70px;
@@ -5495,7 +5495,7 @@ body.admin.modal-open .navbar {
background: rgba(0, 0, 0, .5)
}
-#RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #deleteModal:before {
+#RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #ClearCacheDialog:before, #deleteModal:before {
content: "\E208";
padding-right: 10px;
display: block;
@@ -5517,18 +5517,18 @@ body.admin.modal-open .navbar {
z-index: 99
}
-#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #deleteModal.in:before {
+#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.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, #deleteModal > .modal-dialog {
+#RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog {
width: 450px;
margin: auto
}
-#RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
+#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 {
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);
@@ -5539,7 +5539,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, #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, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header, #deleteModal > .modal-dialog > .modal-content > .modal-header {
padding: 15px 20px;
border-radius: 3px 3px 0 0;
line-height: 1.71428571;
@@ -5552,7 +5552,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, #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, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before, #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
padding-right: 10px;
font-size: 18px;
color: #999;
@@ -5576,6 +5576,11 @@ body.admin.modal-open .navbar {
font-family: plex-icons-new, serif
}
+#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before {
+ content: "\EA15";
+ font-family: plex-icons-new, serif
+}
+
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D";
font-family: plex-icons-new, serif
@@ -5599,6 +5604,12 @@ 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;
@@ -5629,7 +5640,17 @@ body.admin.modal-open .navbar {
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, #deleteModal > .modal-dialog > .modal-content > .modal-body > p {
+#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 {
padding: 20px 20px 0 0;
font-size: 16px;
line-height: 1.6em;
@@ -5638,7 +5659,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), #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), #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache), #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
float: right;
z-index: 9;
position: relative;
@@ -5674,6 +5695,18 @@ body.admin.modal-open .navbar {
border-radius: 3px
}
+#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > #clear_cache {
+ float: right;
+ z-index: 9;
+ position: relative;
+ margin: 25px 0 0 10px;
+ min-width: 80px;
+ padding: 10px 18px;
+ font-size: 16px;
+ line-height: 1.33;
+ border-radius: 3px
+}
+
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
float: right;
z-index: 9;
@@ -5694,11 +5727,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, #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, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache):hover, #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover {
background-color: hsla(0, 0%, 100%, .3)
}
@@ -5732,6 +5769,21 @@ 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;
@@ -7322,11 +7374,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, #deleteModal > .modal-dialog {
+ #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog {
max-width: calc(100vw - 40px)
}
- #RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
+ #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 {
max-width: calc(100vw - 40px);
left: 0
}
@@ -7476,7 +7528,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, #deleteModal.in:before {
+ #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before {
left: auto;
right: 34px
}
diff --git a/cps/static/js/main.js b/cps/static/js/main.js
index 3fbaed88..d8c1863f 100644
--- a/cps/static/js/main.js
+++ b/cps/static/js/main.js
@@ -405,6 +405,18 @@ $(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");
diff --git a/cps/tasks/database.py b/cps/tasks/database.py
new file mode 100644
index 00000000..11f0186d
--- /dev/null
+++ b/cps/tasks/database.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
+# Copyright (C) 2020 mmonkey
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see