Simplified all of the thumbnail generation and loading.

This commit is contained in:
mmonkey 2021-09-24 03:11:14 -05:00
parent 524ed07a6c
commit be28a91315
16 changed files with 111 additions and 376 deletions

View File

@ -57,17 +57,9 @@ class FileSystem:
def get_cache_file_path(self, filename, cache_type=None): def get_cache_file_path(self, filename, cache_type=None):
return join(self.get_cache_dir(cache_type), filename) if filename else None return join(self.get_cache_dir(cache_type), filename) if filename else None
def list_cache_files(self, cache_type=None): def get_cache_file_exists(self, filename, cache_type=None):
path = self.get_cache_dir(cache_type) path = self.get_cache_file_path(filename, cache_type)
return [file for file in listdir(path) if isfile(join(path, file))] return isfile(path)
def list_existing_cache_files(self, filenames, cache_type=None):
path = self.get_cache_dir(cache_type)
return [file for file in listdir(path) if isfile(join(path, file)) and file in filenames]
def list_missing_cache_files(self, filenames, cache_type=None):
path = self.get_cache_dir(cache_type)
return [file for file in listdir(path) if isfile(join(path, file)) and file not in filenames]
def delete_cache_dir(self, cache_type=None): def delete_cache_dir(self, cache_type=None):
if not cache_type and isdir(self._cache_dir): if not cache_type and isdir(self._cache_dir):

View File

@ -35,7 +35,7 @@ from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort, url_for from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, text, func from sqlalchemy.sql.expression import true, false, and_, or_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from markupsafe import escape from markupsafe import escape
@ -550,26 +550,6 @@ def delete_book(book, calibrepath, book_format):
return delete_book_file(book, calibrepath, book_format) return delete_book_file(book, calibrepath, book_format)
def get_thumbnails_for_books(books):
books_with_covers = list(filter(lambda b: b.has_cover, books))
book_ids = list(map(lambda b: b.id, books_with_covers))
cache = fs.FileSystem()
thumbnail_files = cache.list_cache_files(fs.CACHE_TYPE_THUMBNAILS)
thumbnails = ub.session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.book_id.in_(book_ids))\
.filter(ub.Thumbnail.expiration > datetime.utcnow())\
.all()
return list(filter(lambda t: t.filename in thumbnail_files, thumbnails))
def get_thumbnails_for_book_series(series):
books = list(map(lambda s: s[0], series))
return get_thumbnails_for_books(books)
def get_cover_on_failure(use_generic_cover): def get_cover_on_failure(use_generic_cover):
if use_generic_cover: if use_generic_cover:
return send_from_directory(_STATIC_DIR, "generic_cover.jpg") return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
@ -577,9 +557,9 @@ def get_cover_on_failure(use_generic_cover):
return None return None
def get_book_cover(book_id): def get_book_cover(book_id, resolution=None):
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) return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
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):
@ -587,37 +567,6 @@ 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_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_cached_book_cover_thumbnail(cache_id):
parts = cache_id.split('_')
thumbnail_uuid = parts[0] if len(parts) else None
thumbnail = None
if thumbnail_uuid:
thumbnail = ub.session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.uuid == thumbnail_uuid)\
.first()
if thumbnail and thumbnail.expiration > datetime.utcnow():
cache = fs.FileSystem()
if cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename)
elif thumbnail:
book = calibre_db.get_book(thumbnail.book_id)
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
else:
return get_cover_on_failure(True)
def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None): 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:
@ -626,7 +575,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
thumbnail = get_book_cover_thumbnail(book, resolution) thumbnail = get_book_cover_thumbnail(book, resolution)
if thumbnail: if thumbnail:
cache = fs.FileSystem() cache = fs.FileSystem()
if cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS): if cache.get_cache_file_exists(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename) return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename)
# Send the book cover from Google Drive if configured # Send the book cover from Google Drive if configured
@ -661,7 +610,7 @@ def get_book_cover_thumbnail(book, resolution):
.query(ub.Thumbnail)\ .query(ub.Thumbnail)\
.filter(ub.Thumbnail.book_id == book.id)\ .filter(ub.Thumbnail.book_id == book.id)\
.filter(ub.Thumbnail.resolution == resolution)\ .filter(ub.Thumbnail.resolution == resolution)\
.filter(ub.Thumbnail.expiration > datetime.utcnow())\ .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\
.first() .first()

View File

@ -33,6 +33,7 @@ from flask_babel import get_locale
from flask_login import current_user from flask_login import current_user
from markupsafe import escape from markupsafe import escape
from . import logger from . import logger
from .tasks.thumbnail import THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X
jinjia = Blueprint('jinjia', __name__) jinjia = Blueprint('jinjia', __name__)
@ -140,24 +141,17 @@ def uuidfilter(var):
return uuid4() return uuid4()
@jinjia.app_template_filter('book_cover_cache_id') @jinjia.app_template_filter('last_modified')
def book_cover_cache_id(book, resolution=None): def book_cover_cache_id(book):
timestamp = int(book.last_modified.timestamp() * 1000) timestamp = int(book.last_modified.timestamp() * 1000)
cache_bust = str(book.uuid) + '_' + str(timestamp) return str(timestamp)
return cache_bust if not resolution else cache_bust + '_' + str(resolution)
@jinjia.app_template_filter('get_book_thumbnails') @jinjia.app_template_filter('get_cover_srcset')
def get_book_thumbnails(book_id, thumbnails=None): def get_cover_srcset(book):
return list(filter(lambda t: t.book_id == book_id, thumbnails)) if book_id > -1 and thumbnails else list()
@jinjia.app_template_filter('get_book_thumbnail_srcset')
def get_book_thumbnail_srcset(thumbnails):
srcset = list() srcset = list()
for thumbnail in thumbnails: for resolution in [THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X]:
timestamp = int(thumbnail.generated_at.timestamp() * 1000) timestamp = int(book.last_modified.timestamp() * 1000)
cache_id = str(thumbnail.uuid) + '_' + str(timestamp) url = url_for('web.get_cover', book_id=book.id, resolution=resolution, cache_bust=str(timestamp))
url = url_for('web.get_cached_cover_thumbnail', cache_id=cache_id) srcset.append(f'{url} {resolution}x')
srcset.append(url + ' ' + str(thumbnail.resolution) + 'x')
return ', '.join(srcset) return ', '.join(srcset)

View File

@ -21,7 +21,7 @@ from __future__ import division, print_function, unicode_literals
from .services.background_scheduler import BackgroundScheduler from .services.background_scheduler import BackgroundScheduler
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks.database import TaskReconnectDatabase from .tasks.database import TaskReconnectDatabase
from .tasks.thumbnail import TaskSyncCoverThumbnailCache, TaskGenerateCoverThumbnails from .tasks.thumbnail import TaskGenerateCoverThumbnails
def register_jobs(): def register_jobs():
@ -31,9 +31,6 @@ def register_jobs():
# Reconnect metadata.db once every 12 hours # Reconnect metadata.db once every 12 hours
scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16') scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', hour='4,16')
# Cleanup book cover cache once every 24 hours
scheduler.add_task(user=None, task=lambda: TaskSyncCoverThumbnailCache(), trigger='cron', hour=4)
# Generate all missing book cover thumbnails once every 24 hours # Generate all missing book cover thumbnails once every 24 hours
scheduler.add_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4) scheduler.add_task(user=None, task=lambda: TaskGenerateCoverThumbnails(), trigger='cron', hour=4)

View File

@ -22,7 +22,7 @@ import os
from cps import config, db, fs, gdriveutils, logger, ub from cps import config, db, fs, gdriveutils, logger, ub
from cps.services.worker import CalibreTask from cps.services.worker import CalibreTask
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import func from sqlalchemy import or_
try: try:
from urllib.request import urlopen from urllib.request import urlopen
@ -41,9 +41,8 @@ THUMBNAIL_RESOLUTION_3X = 3
class TaskGenerateCoverThumbnails(CalibreTask): class TaskGenerateCoverThumbnails(CalibreTask):
def __init__(self, limit=100, task_message=u'Generating cover thumbnails'): def __init__(self, task_message=u'Generating cover thumbnails'):
super(TaskGenerateCoverThumbnails, self).__init__(task_message) super(TaskGenerateCoverThumbnails, self).__init__(task_message)
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()
self.calibre_db = db.CalibreDB(expire_on_commit=False) self.calibre_db = db.CalibreDB(expire_on_commit=False)
@ -55,74 +54,51 @@ class TaskGenerateCoverThumbnails(CalibreTask):
def run(self, worker_thread): def run(self, worker_thread):
if self.calibre_db.session and use_IM: if self.calibre_db.session and use_IM:
expired_thumbnails = self.get_expired_thumbnails() books_with_covers = self.get_books_with_covers()
thumbnail_book_ids = self.get_thumbnail_book_ids() count = len(books_with_covers)
books_without_thumbnails = self.get_books_without_thumbnails(thumbnail_book_ids)
count = len(books_without_thumbnails) updated = 0
if count == 0: generated = 0
# Do not display this task on the frontend if there are no covers to update for i, book in enumerate(books_with_covers):
self.self_cleanup = True book_cover_thumbnails = self.get_book_cover_thumbnails(book.id)
for i, book in enumerate(books_without_thumbnails): # Generate new thumbnails for missing covers
for resolution in self.resolutions: resolutions = list(map(lambda t: t.resolution, book_cover_thumbnails))
expired_thumbnail = self.get_expired_thumbnail_for_book_and_resolution( missing_resolutions = list(set(self.resolutions).difference(resolutions))
book, for resolution in missing_resolutions:
resolution, generated += 1
expired_thumbnails self.create_book_cover_thumbnail(book, resolution)
)
if expired_thumbnail:
self.update_book_thumbnail(book, expired_thumbnail)
else:
self.create_book_thumbnail(book, resolution)
self.message = u'Generating cover thumbnail {0} of {1}'.format(i + 1, count) # Replace outdated or missing thumbnails
for thumbnail in book_cover_thumbnails:
if book.last_modified > thumbnail.generated_at:
updated += 1
self.update_book_cover_thumbnail(book, thumbnail)
elif not self.cache.get_cache_file_exists(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
updated += 1
self.update_book_cover_thumbnail(book, thumbnail)
self.message = u'Processing book {0} of {1}'.format(i + 1, count)
self.progress = (1.0 / count) * i self.progress = (1.0 / count) * i
self._handleSuccess() self._handleSuccess()
self.app_db_session.remove() self.app_db_session.remove()
def get_expired_thumbnails(self): def get_books_with_covers(self):
return self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.expiration < datetime.utcnow())\
.all()
def get_thumbnail_book_ids(self):
return self.app_db_session\
.query(ub.Thumbnail.book_id)\
.group_by(ub.Thumbnail.book_id)\
.having(func.min(ub.Thumbnail.expiration) > datetime.utcnow())\
.distinct()
def get_books_without_thumbnails(self, thumbnail_book_ids):
return self.calibre_db.session\ return self.calibre_db.session\
.query(db.Books)\ .query(db.Books)\
.filter(db.Books.has_cover == 1)\ .filter(db.Books.has_cover == 1)\
.filter(db.Books.id.notin_(thumbnail_book_ids))\
.limit(self.limit)\
.all() .all()
def get_expired_thumbnail_for_book_and_resolution(self, book, resolution, expired_thumbnails): def get_book_cover_thumbnails(self, book_id):
for thumbnail in expired_thumbnails: return self.app_db_session\
if thumbnail.book_id == book.id and thumbnail.resolution == resolution: .query(ub.Thumbnail)\
return thumbnail .filter(ub.Thumbnail.book_id == book_id)\
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\
.all()
return None def create_book_cover_thumbnail(self, book, resolution):
def update_book_thumbnail(self, book, thumbnail):
thumbnail.generated_at = datetime.utcnow()
thumbnail.expiration = datetime.utcnow() + timedelta(days=30)
try:
self.app_db_session.commit()
self.generate_book_thumbnail(book, thumbnail)
except Exception as ex:
self.log.info(u'Error updating book thumbnail: ' + str(ex))
self._handleError(u'Error updating book thumbnail: ' + str(ex))
self.app_db_session.rollback()
def create_book_thumbnail(self, book, resolution):
thumbnail = ub.Thumbnail() thumbnail = ub.Thumbnail()
thumbnail.book_id = book.id thumbnail.book_id = book.id
thumbnail.format = 'jpeg' thumbnail.format = 'jpeg'
@ -137,6 +113,18 @@ class TaskGenerateCoverThumbnails(CalibreTask):
self._handleError(u'Error creating book thumbnail: ' + str(ex)) self._handleError(u'Error creating book thumbnail: ' + str(ex))
self.app_db_session.rollback() self.app_db_session.rollback()
def update_book_cover_thumbnail(self, book, thumbnail):
thumbnail.generated_at = datetime.utcnow()
try:
self.app_db_session.commit()
self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS)
self.generate_book_thumbnail(book, thumbnail)
except Exception as ex:
self.log.info(u'Error updating book thumbnail: ' + str(ex))
self._handleError(u'Error updating book thumbnail: ' + str(ex))
self.app_db_session.rollback()
def generate_book_thumbnail(self, book, thumbnail): def generate_book_thumbnail(self, book, thumbnail):
if book and thumbnail: if book and thumbnail:
if config.config_use_google_drive: if config.config_use_google_drive:
@ -190,128 +178,6 @@ class TaskGenerateCoverThumbnails(CalibreTask):
return "ThumbnailsGenerate" return "ThumbnailsGenerate"
class TaskSyncCoverThumbnailCache(CalibreTask):
def __init__(self, task_message=u'Syncing cover thumbnail cache'):
super(TaskSyncCoverThumbnailCache, self).__init__(task_message)
self.log = logger.create()
self.app_db_session = ub.get_new_session_instance()
self.calibre_db = db.CalibreDB(expire_on_commit=False)
self.cache = fs.FileSystem()
def run(self, worker_thread):
cached_thumbnail_files = self.cache.list_cache_files(fs.CACHE_TYPE_THUMBNAILS)
# Expire thumbnails in the database if the cached file is missing
# This case will happen if a user deletes the cache dir or cached files
if self.app_db_session:
self.expire_missing_thumbnails(cached_thumbnail_files)
self.progress = 0.25
# Delete thumbnails in the database if the book has been removed
# This case will happen if a book is removed in Calibre and the metadata.db file is updated in the filesystem
if self.app_db_session and self.calibre_db:
book_ids = self.get_book_ids()
self.delete_thumbnails_for_missing_books(book_ids)
self.progress = 0.50
# Expire thumbnails in the database if their corresponding book has been updated since they were generated
# This case will happen if the book was updated externally
if self.app_db_session and self.cache:
books = self.get_books_updated_in_the_last_day()
book_ids = list(map(lambda b: b.id, books))
thumbnails = self.get_thumbnails_for_updated_books(book_ids)
self.expire_thumbnails_for_updated_book(books, thumbnails)
self.progress = 0.75
# Delete extraneous cached thumbnail files
# This case will happen if a book was deleted and the thumbnail OR the metadata.db file was changed externally
if self.app_db_session:
db_thumbnail_files = self.get_thumbnail_filenames()
self.delete_extraneous_thumbnail_files(cached_thumbnail_files, db_thumbnail_files)
self._handleSuccess()
self.app_db_session.remove()
def expire_missing_thumbnails(self, filenames):
try:
self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.filename.notin_(filenames))\
.update({"expiration": datetime.utcnow()}, synchronize_session=False)
self.app_db_session.commit()
except Exception as ex:
self.log.info(u'Error expiring thumbnails for missing cache files: ' + str(ex))
self._handleError(u'Error expiring thumbnails for missing cache files: ' + str(ex))
self.app_db_session.rollback()
def get_book_ids(self):
results = self.calibre_db.session\
.query(db.Books.id)\
.filter(db.Books.has_cover == 1)\
.distinct()
return [value for value, in results]
def delete_thumbnails_for_missing_books(self, book_ids):
try:
self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.book_id.notin_(book_ids))\
.delete(synchronize_session=False)
self.app_db_session.commit()
except Exception as ex:
self.log.info(str(ex))
self._handleError(u'Error deleting thumbnails for missing books: ' + str(ex))
self.app_db_session.rollback()
def get_thumbnail_filenames(self):
results = self.app_db_session\
.query(ub.Thumbnail.filename)\
.all()
return [thumbnail for thumbnail, in results]
def delete_extraneous_thumbnail_files(self, cached_thumbnail_files, db_thumbnail_files):
extraneous_files = list(set(cached_thumbnail_files).difference(db_thumbnail_files))
for file in extraneous_files:
self.cache.delete_cache_file(file, fs.CACHE_TYPE_THUMBNAILS)
def get_books_updated_in_the_last_day(self):
return self.calibre_db.session\
.query(db.Books)\
.filter(db.Books.has_cover == 1)\
.filter(db.Books.last_modified > datetime.utcnow() - timedelta(days=1, hours=1))\
.all()
def get_thumbnails_for_updated_books(self, book_ids):
return self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.book_id.in_(book_ids))\
.all()
def expire_thumbnails_for_updated_book(self, books, thumbnails):
thumbnail_ids = list()
for book in books:
for thumbnail in thumbnails:
if thumbnail.book_id == book.id and thumbnail.generated_at < book.last_modified:
thumbnail_ids.append(thumbnail.id)
try:
self.app_db_session\
.query(ub.Thumbnail)\
.filter(ub.Thumbnail.id.in_(thumbnail_ids)) \
.update({"expiration": datetime.utcnow()}, synchronize_session=False)
self.app_db_session.commit()
except Exception as ex:
self.log.info(u'Error expiring thumbnails for updated books: ' + str(ex))
self._handleError(u'Error expiring thumbnails for updated books: ' + str(ex))
self.app_db_session.rollback()
@property
def name(self):
return "ThumbnailsSync"
class TaskClearCoverThumbnailCache(CalibreTask): class TaskClearCoverThumbnailCache(CalibreTask):
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'): def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
super(TaskClearCoverThumbnailCache, self).__init__(task_message) super(TaskClearCoverThumbnailCache, self).__init__(task_message)
@ -325,9 +191,9 @@ class TaskClearCoverThumbnailCache(CalibreTask):
if self.book_id: if self.book_id:
thumbnails = self.get_thumbnails_for_book(self.book_id) thumbnails = self.get_thumbnails_for_book(self.book_id)
for thumbnail in thumbnails: for thumbnail in thumbnails:
self.expire_and_delete_thumbnail(thumbnail) self.delete_thumbnail(thumbnail)
else: else:
self.expire_and_delete_all_thumbnails() self.delete_all_thumbnails()
self._handleSuccess() self._handleSuccess()
self.app_db_session.remove() self.app_db_session.remove()
@ -338,29 +204,19 @@ class TaskClearCoverThumbnailCache(CalibreTask):
.filter(ub.Thumbnail.book_id == book_id)\ .filter(ub.Thumbnail.book_id == book_id)\
.all() .all()
def expire_and_delete_thumbnail(self, thumbnail): def delete_thumbnail(self, thumbnail):
thumbnail.expiration = datetime.utcnow()
try: try:
self.app_db_session.commit()
self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS) self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS)
except Exception as ex: except Exception as ex:
self.log.info(u'Error expiring book thumbnail: ' + str(ex)) self.log.info(u'Error deleting book thumbnail: ' + str(ex))
self._handleError(u'Error expiring book thumbnail: ' + str(ex)) self._handleError(u'Error deleting 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()})
def delete_all_thumbnails(self):
try: try:
self.app_db_session.commit()
self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS) self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS)
except Exception as ex: except Exception as ex:
self.log.info(u'Error expiring book thumbnails: ' + str(ex)) self.log.info(u'Error deleting book thumbnails: ' + str(ex))
self._handleError(u'Error expiring book thumbnails: ' + str(ex)) self._handleError(u'Error deleting book thumbnails: ' + str(ex))
self.app_db_session.rollback()
@property @property
def name(self): def name(self):

View File

@ -37,8 +37,7 @@
<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) }}">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry, title=author.name|safe) }}
<!-- <img title="{{author.name|safe}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" /> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -1,14 +1,11 @@
{% macro book_cover_image(book, thumbnails, title=None) -%} {% macro book_cover_image(book, title=None) -%}
{%- set book_title = book.title if book.title else book.name -%} {%- set book_title = book.title if book.title else book.name -%}
{%- set book_title = title if title else book_title -%} {%- set book_title = title if title else book_title -%}
{% set srcset = thumbnails|get_book_thumbnail_srcset if thumbnails|length else '' %} {% set srcset = book|get_cover_srcset %}
{%- if srcset|length -%} <img
<img srcset="{{ srcset }}"
srcset="{{ srcset }}" src="{{ url_for('web.get_cover', book_id=book.id, resolution=0, cache_bust=book|last_modified) }}"
src="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id) }}" title="{{ book_title }}"
alt="{{ book_title }}" alt="{{ book_title }}"
/> />
{%- else -%}
<img src="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id) }}" alt="{{ book_title }}" />
{%- endif -%}
{%- 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, book.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(book) }}
<!-- <img id="detailcover" title="{{book.title}}" 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, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img id="detailcover" title="{{entry.title}}" 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

@ -10,8 +10,7 @@
{% 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">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -29,8 +29,7 @@
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
<span class="img"> <span class="img">
{{ book_cover_image(entry[0], entry[0].id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry[0], title=entry[0].series[0].name|shortentitle) }}
<!-- <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/> -->
<span class="badge">{{entry.count}}</span> <span class="badge">{{entry.count}}</span>
</span> </span>
</a> </a>

View File

@ -10,8 +10,7 @@
<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">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img title="{{ entry.title }}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
@ -88,8 +87,7 @@
<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">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img title="{{ entry.title }}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -45,8 +45,7 @@
{% 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">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -32,8 +32,7 @@
<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">
<span class="img"> <span class="img">
{{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }} {{ book_cover_image(entry) }}
<!-- <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> -->
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>

View File

@ -532,7 +532,7 @@ class Thumbnail(Base):
resolution = Column(SmallInteger, default=1) resolution = Column(SmallInteger, default=1)
filename = Column(String, default=filename) filename = Column(String, default=filename)
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow()) generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
expiration = Column(DateTime, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(days=90)) expiration = Column(DateTime, nullable=True)
# Add missing tables during migration of database # Add missing tables during migration of database

View File

@ -51,7 +51,6 @@ from . import babel, db, ub, config, get_locale, app
from . import calibre_db from . import calibre_db
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, render_task_status, check_email, check_username, \ from .helper import check_valid_domain, render_task_status, check_email, check_username, \
get_cached_book_cover, get_cached_book_cover_thumbnail, get_thumbnails_for_books, get_thumbnails_for_book_series, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email
from .pagination import Pagination from .pagination import Pagination
@ -415,10 +414,8 @@ def render_books_list(data, sort, book_id, page):
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book, db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Books"), page=website, thumbnails=thumbnails) title=_(u"Books"), page=website)
def render_rated_books(page, book_id, order): def render_rated_books(page, book_id, order):
@ -467,9 +464,8 @@ def render_hot_books(page):
ub.delete_download(book.Downloads.book_id) ub.delete_download(book.Downloads.book_id)
numBooks = entries.__len__() numBooks = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks) pagination = Pagination(page, config.config_books_per_page, numBooks)
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Hot Books (Most Downloaded)"), page="hot", thumbnails=thumbnails) title=_(u"Hot Books (Most Downloaded)"), page="hot")
else: else:
abort(404) abort(404)
@ -497,16 +493,13 @@ def render_downloaded_books(page, order, user_id):
.filter(db.Books.id == book.id).first(): .filter(db.Books.id == book.id).first():
ub.delete_download(book.id) ub.delete_download(book.id)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first() user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', return render_title_template('index.html',
random=random, random=random,
entries=entries, entries=entries,
pagination=pagination, pagination=pagination,
id=user_id, id=user_id,
title=_(u"Downloaded books by %(user)s",user=user.name), title=_(u"Downloaded books by %(user)s",user=user.name),
page="download", page="download")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -535,10 +528,9 @@ def render_author_books(page, author_id, order):
author_info = services.goodreads_support.get_author_info(author_name) author_info = services.goodreads_support.get_author_info(author_name)
other_books = services.goodreads_support.get_other_books(author_info, entries) other_books = services.goodreads_support.get_other_books(author_info, entries)
thumbnails = get_thumbnails_for_books(entries)
return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id, return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id,
title=_(u"Author: %(name)s", name=author_name), author=author_info, title=_(u"Author: %(name)s", name=author_name), author=author_info,
other_books=other_books, page="author", thumbnails=thumbnails) other_books=other_books, page="author")
def render_publisher_books(page, book_id, order): def render_publisher_books(page, book_id, order):
@ -551,10 +543,8 @@ def render_publisher_books(page, book_id, order):
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book, db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher", title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -566,10 +556,8 @@ def render_series_books(page, book_id, order):
db.Books, db.Books,
db.Books.series.any(db.Series.id == book_id), db.Books.series.any(db.Series.id == book_id),
[order[0]]) [order[0]])
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"Series: %(serie)s", serie=name.name), page="series", title=_(u"Series: %(serie)s", serie=name.name), page="series")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -581,10 +569,8 @@ def render_ratings_books(page, book_id, order):
db.Books.ratings.any(db.Ratings.id == book_id), db.Books.ratings.any(db.Ratings.id == book_id),
[order[0]]) [order[0]])
if name and name.rating <= 10: if name and name.rating <= 10:
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), page="ratings", title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), page="ratings")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -596,10 +582,8 @@ def render_formats_books(page, book_id, order):
db.Books, db.Books,
db.Books.data.any(db.Data.format == book_id.upper()), db.Books.data.any(db.Data.format == book_id.upper()),
[order[0]]) [order[0]])
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"File format: %(format)s", format=name.format), page="formats", title=_(u"File format: %(format)s", format=name.format), page="formats")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -614,10 +598,8 @@ def render_category_books(page, book_id, order):
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book, db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Category: %(name)s", name=name.name), page="category", title=_(u"Category: %(name)s", name=name.name), page="category")
thumbnails=thumbnails)
else: else:
abort(404) abort(404)
@ -635,9 +617,8 @@ def render_language_books(page, name, order):
db.Books, db.Books,
db.Books.languages.any(db.Languages.lang_code == name), db.Books.languages.any(db.Languages.lang_code == name),
[order[0]]) [order[0]])
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language", thumbnails=thumbnails) title=_(u"Language: %(name)s", name=lang_name), page="language")
def render_read_books(page, are_read, as_xml=False, order=None): def render_read_books(page, are_read, as_xml=False, order=None):
@ -687,10 +668,8 @@ def render_read_books(page, are_read, as_xml=False, order=None):
else: else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')' name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread" pagename = "unread"
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename, thumbnails=thumbnails) title=name, page=pagename)
def render_archived_books(page, order): def render_archived_books(page, order):
@ -713,9 +692,8 @@ def render_archived_books(page, order):
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')' name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived" pagename = "archived"
thumbnails = get_thumbnails_for_books(entries + random if type(random) is list else entries)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename, thumbnails=thumbnails) title=name, page=pagename)
def render_prepare_search_form(cc): def render_prepare_search_form(cc):
@ -752,7 +730,6 @@ def render_prepare_search_form(cc):
def render_search_results(term, offset=None, order=None, limit=None): def render_search_results(term, offset=None, order=None, limit=None):
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit, *join) entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit, *join)
thumbnails = get_thumbnails_for_books(entries)
return render_title_template('search.html', return render_title_template('search.html',
searchterm=term, searchterm=term,
pagination=pagination, pagination=pagination,
@ -761,8 +738,7 @@ def render_search_results(term, offset=None, order=None, limit=None):
entries=entries, entries=entries,
result_count=result_count, result_count=result_count,
title=_(u"Search"), title=_(u"Search"),
page="search", page="search")
thumbnails=thumbnails)
# ################################### View Books list ################################################################## # ################################### View Books list ##################################################################
@ -973,10 +949,9 @@ def series_list():
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
thumbnails = get_thumbnails_for_book_series(entries)
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist, return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view", title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no, thumbnails=thumbnails) order=order_no)
else: else:
abort(404) abort(404)
@ -1392,17 +1367,13 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
else: else:
offset = 0 offset = 0
limit_all = result_count limit_all = result_count
entries = q[offset:limit_all]
thumbnails = get_thumbnails_for_books(entries)
return render_title_template('search.html', return render_title_template('search.html',
adv_searchterm=searchterm, adv_searchterm=searchterm,
pagination=pagination, pagination=pagination,
entries=entries, entries=q[offset:limit_all],
result_count=result_count, result_count=result_count,
title=_(u"Advanced Search"), title=_(u"Advanced Search"),
page="advsearch", page="advsearch")
thumbnails=thumbnails)
@web.route("/advsearch", methods=['GET']) @web.route("/advsearch", methods=['GET'])
@ -1417,21 +1388,11 @@ def advanced_search_form():
@web.route("/cover/<int:book_id>") @web.route("/cover/<int:book_id>")
@web.route("/cover/<int:book_id>/<int:resolution>")
@web.route("/cover/<int:book_id>/<int:resolution>/<string:cache_bust>")
@login_required_if_no_ano @login_required_if_no_ano
def get_cover(book_id): def get_cover(book_id, resolution=None, cache_bust=None):
return get_book_cover(book_id) 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("/cached-cover-thumbnail/<string:cache_id>")
@login_required_if_no_ano
def get_cached_cover_thumbnail(cache_id):
return get_cached_book_cover_thumbnail(cache_id)
@web.route("/robots.txt") @web.route("/robots.txt")
@ -1841,7 +1802,6 @@ def show_book(book_id):
if media_format.format.lower() in constants.EXTENSIONS_AUDIO: if media_format.format.lower() in constants.EXTENSIONS_AUDIO:
audioentries.append(media_format.format.lower()) audioentries.append(media_format.format.lower())
thumbnails = get_thumbnails_for_books([entries])
return render_title_template('detail.html', return render_title_template('detail.html',
entry=entries, entry=entries,
audioentries=audioentries, audioentries=audioentries,
@ -1853,8 +1813,7 @@ def show_book(book_id):
is_archived=is_archived, is_archived=is_archived,
kindle_list=kindle_list, kindle_list=kindle_list,
reader_list=reader_list, reader_list=reader_list,
page="book", page="book")
thumbnails=thumbnails)
else: else:
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible") log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),