1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-01 13:59:57 +00:00

Added clear cache button to admin settings, updated cache busting for book cover images

This commit is contained in:
mmonkey 2020-12-23 03:25:25 -06:00
parent 541fc7e14e
commit 626051e489
20 changed files with 278 additions and 57 deletions

View File

@ -38,7 +38,7 @@ from sqlalchemy import and_
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_ 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 . 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 .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
from .gdriveutils import is_gdrive_ready, gdrive_support from .gdriveutils import is_gdrive_ready, gdrive_support
@ -157,6 +157,23 @@ def shutdown():
return json.dumps(showtext), 400 return json.dumps(showtext), 400
@admi.route("/clear-cache")
@login_required
@admin_required
def clear_cache():
cache_type = request.args.get('cache_type'.strip())
showtext = {}
if cache_type == 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") @admi.route("/admin/view")
@login_required @login_required
@admin_required @admin_required

View File

@ -595,6 +595,7 @@ def upload_cover(request, book):
abort(403) abort(403)
ret, message = helper.save_cover(requested_file, book.path) ret, message = helper.save_cover(requested_file, book.path)
if ret is True: if ret is True:
helper.clear_cover_thumbnail_cache(book.id)
return True return True
else: else:
flash(message, category="error") flash(message, category="error")
@ -684,6 +685,7 @@ def edit_book(book_id):
if result is True: if result is True:
book.has_cover = 1 book.has_cover = 1
modif_date = True modif_date = True
helper.clear_cover_thumbnail_cache(book.id)
else: else:
flash(error, category="error") flash(error, category="error")

View File

@ -58,6 +58,7 @@ from .constants import STATIC_DIR as _STATIC_DIR
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
from .tasks.mail import TaskEmail from .tasks.mail import TaskEmail
from .tasks.thumbnail import TaskClearCoverThumbnailCache
log = logger.create() 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): def delete_book(book, calibrepath, book_format):
clear_cover_thumbnail_cache(book.id)
if config.config_use_google_drive: if config.config_use_google_drive:
return delete_book_gdrive(book, book_format) return delete_book_gdrive(book, book_format)
else: else:
@ -538,9 +540,9 @@ def get_cover_on_failure(use_generic_cover):
return None 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) 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): 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) 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: if book and book.has_cover:
# Send the book cover thumbnail if it exists in cache # Send the book cover thumbnail if it exists in cache
if not disable_thumbnail: if resolution:
thumbnail = get_book_cover_thumbnail(book, resolution) thumbnail = get_book_cover_thumbnail(book, resolution)
if thumbnail: if thumbnail:
cache = fs.FileSystem() 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) 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: if book and book.has_cover:
return ub.session\ return ub.session\
.query(ub.Thumbnail)\ .query(ub.Thumbnail)\
@ -846,3 +856,6 @@ def get_download_link(book_id, book_format, client):
else: else:
abort(404) abort(404)
def clear_cover_thumbnail_cache(book_id=None):
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))

View File

@ -128,8 +128,14 @@ def formatseriesindex_filter(series_index):
return series_index return series_index
return 0 return 0
@jinjia.app_template_filter('uuidfilter') @jinjia.app_template_filter('uuidfilter')
def uuidfilter(var): def uuidfilter(var):
return uuid4() 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)

View File

@ -18,12 +18,10 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
from . import config, db, logger, ub
from .services.background_scheduler import BackgroundScheduler from .services.background_scheduler import BackgroundScheduler
from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskCleanupCoverThumbnailCache, TaskGenerateCoverThumbnails from .tasks.thumbnail import TaskCleanupCoverThumbnailCache, TaskGenerateCoverThumbnails
log = logger.create()
def register_jobs(): def register_jobs():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
@ -35,10 +33,4 @@ def register_jobs():
scheduler.add_task(user=None, task=lambda: TaskCleanupCoverThumbnailCache(), trigger='cron', hour=4) scheduler.add_task(user=None, task=lambda: TaskCleanupCoverThumbnailCache(), trigger='cron', hour=4)
# Reconnect metadata.db every 4 hours # Reconnect metadata.db every 4 hours
scheduler.add(func=reconnect_db_job, trigger='interval', hours=4) scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), 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)

View File

@ -5167,7 +5167,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, #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 cursor: pointer
} }
@ -5254,7 +5254,7 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
margin-bottom: 20px margin-bottom: 20px
} }
body.admin:not(.modal-open) .btn-default { body.admin .btn-default {
margin-bottom: 10px margin-bottom: 10px
} }
@ -5485,7 +5485,7 @@ body.admin.modal-open .navbar {
z-index: 0 !important z-index: 0 !important
} }
#RestartDialog, #ShutdownDialog, #StatusDialog, #deleteModal { #RestartDialog, #ShutdownDialog, #StatusDialog, #ClearCacheDialog, #deleteModal {
top: 0; top: 0;
overflow: hidden; overflow: hidden;
padding-top: 70px; padding-top: 70px;
@ -5495,7 +5495,7 @@ body.admin.modal-open .navbar {
background: rgba(0, 0, 0, .5) 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"; content: "\E208";
padding-right: 10px; padding-right: 10px;
display: block; display: block;
@ -5517,18 +5517,18 @@ body.admin.modal-open .navbar {
z-index: 99 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); -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, #deleteModal > .modal-dialog { #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .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, #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); 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);
@ -5539,7 +5539,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, #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; padding: 15px 20px;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
line-height: 1.71428571; line-height: 1.71428571;
@ -5552,7 +5552,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, #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; padding-right: 10px;
font-size: 18px; font-size: 18px;
color: #999; color: #999;
@ -5576,6 +5576,11 @@ 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 {
content: "\EA15";
font-family: plex-icons-new, serif
}
#deleteModal > .modal-dialog > .modal-content > .modal-header:before { #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new, serif font-family: plex-icons-new, serif
@ -5599,6 +5604,12 @@ body.admin.modal-open .navbar {
font-size: 20px 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 { #deleteModal > .modal-dialog > .modal-content > .modal-header:after {
content: "Delete Book"; content: "Delete Book";
display: inline-block; display: inline-block;
@ -5629,7 +5640,17 @@ body.admin.modal-open .navbar {
text-align: left 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; padding: 20px 20px 0 0;
font-size: 16px; font-size: 16px;
line-height: 1.6em; line-height: 1.6em;
@ -5638,7 +5659,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), #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; float: right;
z-index: 9; z-index: 9;
position: relative; position: relative;
@ -5674,6 +5695,18 @@ body.admin.modal-open .navbar {
border-radius: 3px 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 { #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
float: right; float: right;
z-index: 9; z-index: 9;
@ -5694,11 +5727,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) {
margin: 25px 0 0 10px
}
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default { #deleteModal > .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, #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) 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) 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;
@ -7322,11 +7374,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, #deleteModal > .modal-dialog { #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .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, #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); max-width: calc(100vw - 40px);
left: 0 left: 0
} }
@ -7476,7 +7528,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, #deleteModal.in:before { #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before {
left: auto; left: auto;
right: 34px right: 34px
} }

View File

@ -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 // Init all data control handlers to default
$("input[data-control]").trigger("change"); $("input[data-control]").trigger("change");

49
cps/tasks/database.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from cps import config, logger
from cps.services.worker import CalibreTask
try:
from urllib.request import urlopen
except ImportError as e:
from urllib2 import urlopen
class TaskReconnectDatabase(CalibreTask):
def __init__(self, task_message=u'Reconnecting Calibre database'):
super(TaskReconnectDatabase, self).__init__(task_message)
self.log = logger.create()
self.listen_address = config.get_config_ipaddress()
self.listen_port = config.config_port
def run(self, worker_thread):
address = self.listen_address if self.listen_address else 'localhost'
port = self.listen_port if self.listen_port else 8083
try:
urlopen('http://' + address + ':' + str(port) + '/reconnect')
self._handleSuccess()
except Exception as ex:
self._handleError(u'Unable to reconnect Calibre database: ' + str(ex))
@property
def name(self):
return "Reconnect Database"

View File

@ -42,7 +42,6 @@ THUMBNAIL_RESOLUTION_2X = 2
class TaskGenerateCoverThumbnails(CalibreTask): class TaskGenerateCoverThumbnails(CalibreTask):
def __init__(self, limit=100, task_message=u'Generating cover thumbnails'): def __init__(self, limit=100, task_message=u'Generating cover thumbnails'):
super(TaskGenerateCoverThumbnails, self).__init__(task_message) super(TaskGenerateCoverThumbnails, self).__init__(task_message)
self.self_cleanup = True
self.limit = limit self.limit = limit
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()
@ -186,7 +185,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
class TaskCleanupCoverThumbnailCache(CalibreTask): class TaskCleanupCoverThumbnailCache(CalibreTask):
def __init__(self, task_message=u'Validating cover thumbnail cache'): def __init__(self, task_message=u'Cleaning up cover thumbnail cache'):
super(TaskCleanupCoverThumbnailCache, self).__init__(task_message) super(TaskCleanupCoverThumbnailCache, 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()
@ -265,3 +264,58 @@ class TaskCleanupCoverThumbnailCache(CalibreTask):
@property @property
def name(self): def name(self):
return "CleanupCoverThumbnailCache" return "CleanupCoverThumbnailCache"
class TaskClearCoverThumbnailCache(CalibreTask):
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
self.log = logger.create()
self.book_id = book_id
self.app_db_session = ub.get_new_session_instance()
self.cache = fs.FileSystem()
def run(self, worker_thread):
if self.app_db_session:
if self.book_id:
thumbnails = self.get_thumbnails_for_book(self.book_id)
for thumbnail in thumbnails:
self.expire_and_delete_thumbnail(thumbnail)
else:
self.expire_and_delete_all_thumbnails()
self._handleSuccess()
self.app_db_session.remove()
def get_thumbnails_for_book(self, book_id):
return self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.book_id == book_id)\
.all()
def expire_and_delete_thumbnail(self, thumbnail):
thumbnail.expiration = datetime.utcnow()
try:
self.app_db_session.commit()
self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS)
except Exception as ex:
self.log.info(u'Error expiring book thumbnail: ' + str(ex))
self._handleError(u'Error expiring book thumbnail: ' + str(ex))
self.app_db_session.rollback()
def expire_and_delete_all_thumbnails(self):
self.app_db_session\
.query(ub.Thumbnail)\
.update({'expiration': datetime.utcnow()})
try:
self.app_db_session.commit()
self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS)
except Exception as ex:
self.log.info(u'Error expiring book thumbnails: ' + str(ex))
self._handleError(u'Error expiring book thumbnails: ' + str(ex))
self.app_db_session.rollback()
@property
def name(self):
return "ClearCoverThumbnailCache"

View File

@ -139,15 +139,18 @@
</div> </div>
</div> </div>
<div class="row form-group"> <div class="row form-group">
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<div class="btn btn-default"><a id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a></div> <div class="btn btn-default"><a id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a></div>
<div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div> <div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div>
</div> </div>
<div class="row form-group"> <div class="row form-group">
<div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div> <div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div>
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div> <div class="btn btn-default" id="clear_cover_thumbnail_cache" data-toggle="modal" data-target="#ClearCacheDialog">{{_('Clear Cover Thumbnail Cache')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div> </div>
<div class="row form-group">
<div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
</div> </div>
<div class="row"> <div class="row">
@ -226,4 +229,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="ClearCacheDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-info"></div>
<div class="modal-body text-center">
<p>{{_('Are you sure you want to clear the cover thumbnail cache?')}}</p>
<div id="spinner3" class="spinner" style="display:none;">
<img id="img-spinner3" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/>
</div>
<p></p>
<button type="button" class="btn btn-default" id="clear_cache" >{{_('OK')}}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -36,7 +36,7 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -1,8 +1,8 @@
{% macro book_cover_image(book_id, book_title) -%} {% macro book_cover_image(book, book_title) -%}
<img <img
srcset="{{ url_for('web.get_cover', book_id=book_id, resolution=1) }} 1x, srcset="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id(1)) }} 1x,
{{ url_for('web.get_cover', book_id=book_id, resolution=2) }} 2x" {{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id(2)) }} 2x"
src="{{ url_for('web.get_cover', book_id=book_id) }}" src="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id) }}"
alt="{{ book_title }}" alt="{{ book_title }}"
/> />
{%- endmacro %} {%- endmacro %}

View File

@ -4,8 +4,7 @@
{% if book %} {% if book %}
<div class="col-sm-3 col-lg-3 col-xs-12"> <div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover"> <div class="cover">
{{ book_cover_image(book.id, book.title) }} {{ book_cover_image(book, book.title) }}
<!-- <img src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>-->
</div> </div>
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
<div class="text-center"> <div class="text-center">

View File

@ -4,8 +4,7 @@
<div class="row"> <div class="row">
<div class="col-sm-3 col-lg-3 col-xs-5"> <div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover"> <div class="cover">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
<!-- <img src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />-->
</div> </div>
</div> </div>
<div class="col-sm-9 col-lg-9 book-meta"> <div class="col-sm-9 col-lg-9 book-meta">

View File

@ -9,7 +9,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -29,7 +29,7 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}"> <div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
{{ book_cover_image(entry[0].id, entry[0].name) }} {{ book_cover_image(entry[0], entry[0].name) }}
<span class="badge">{{entry.count}}</span> <span class="badge">{{entry.count}}</span>
</a> </a>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand"> <div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
@ -83,7 +83,7 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books"> <div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -44,7 +44,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -31,7 +31,7 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{{ book_cover_image(entry.id, entry.title) }} {{ book_cover_image(entry, entry.title) }}
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -50,7 +50,7 @@ from . import babel, db, ub, config, get_locale, app
from . import calibre_db, shelf from . import calibre_db, shelf
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, render_task_status, \ from .helper import check_valid_domain, render_task_status, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ get_cc_columns, get_book_cover, get_cached_book_cover, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
from .pagination import Pagination from .pagination import Pagination
from .redirect import redirect_back from .redirect import redirect_back
@ -1177,6 +1177,12 @@ def get_cover(book_id, resolution=1):
return get_book_cover(book_id, resolution) return get_book_cover(book_id, resolution)
@web.route("/cached-cover/<string:cache_id>")
@login_required_if_no_ano
def get_cached_cover(cache_id):
return get_cached_book_cover(cache_id)
@web.route("/robots.txt") @web.route("/robots.txt")
def get_robots(): def get_robots():
return send_from_directory(constants.STATIC_DIR, "robots.txt") return send_from_directory(constants.STATIC_DIR, "robots.txt")