From 0bd544704d2060efa544d176b582826838d306ec Mon Sep 17 00:00:00 2001 From: mmonkey Date: Sat, 25 Sep 2021 03:04:38 -0500 Subject: [PATCH] Added series cover thumbnail generation. Better cache file handling. --- cps/admin.py | 2 +- cps/constants.py | 13 ++ cps/fs.py | 19 ++- cps/helper.py | 57 ++++++- cps/jinjia.py | 43 ++++-- cps/schedule.py | 3 +- cps/tasks/thumbnail.py | 278 ++++++++++++++++++++++++++++++---- cps/templates/author.html | 2 +- cps/templates/book_cover.html | 11 -- cps/templates/book_edit.html | 4 +- cps/templates/detail.html | 2 +- cps/templates/discover.html | 4 +- cps/templates/fragment.html | 2 +- cps/templates/grid.html | 4 +- cps/templates/image.html | 23 +++ cps/templates/index.html | 6 +- cps/templates/layout.html | 2 +- cps/templates/search.html | 4 +- cps/templates/shelf.html | 4 +- cps/ub.py | 5 +- cps/web.py | 32 +++- 21 files changed, 429 insertions(+), 91 deletions(-) delete mode 100644 cps/templates/book_cover.html create mode 100644 cps/templates/image.html diff --git a/cps/admin.py b/cps/admin.py index 98535320..92c8bd70 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -166,7 +166,7 @@ def clear_cache(): cache_type = request.args.get('cache_type'.strip()) showtext = {} - if cache_type == fs.CACHE_TYPE_THUMBNAILS: + if cache_type == constants.CACHE_TYPE_THUMBNAILS: log.info('clearing cover thumbnail cache') showtext['text'] = _(u'Cleared cover thumbnail cache') helper.clear_cover_thumbnail_cache() diff --git a/cps/constants.py b/cps/constants.py index 6a6b8f2b..306d2872 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -169,6 +169,19 @@ NIGHTLY_VERSION[1] = '$Format:%cI$' # NIGHTLY_VERSION[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57' # NIGHTLY_VERSION[1] = '2018-09-09T10:13:08+02:00' +# CACHE +CACHE_TYPE_THUMBNAILS = 'thumbnails' + +# Thumbnail Types +THUMBNAIL_TYPE_COVER = 1 +THUMBNAIL_TYPE_SERIES = 2 +THUMBNAIL_TYPE_AUTHOR = 3 + +# Thumbnails Sizes +COVER_THUMBNAIL_ORIGINAL = 0 +COVER_THUMBNAIL_SMALL = 1 +COVER_THUMBNAIL_MEDIUM = 2 +COVER_THUMBNAIL_LARGE = 3 # clean-up the module namespace del sys, os, namedtuple diff --git a/cps/fs.py b/cps/fs.py index 30ab552a..0171a5d5 100644 --- a/cps/fs.py +++ b/cps/fs.py @@ -19,12 +19,10 @@ from __future__ import division, print_function, unicode_literals from . import logger from .constants import CACHE_DIR -from os import listdir, makedirs, remove +from os import makedirs, remove from os.path import isdir, isfile, join from shutil import rmtree -CACHE_TYPE_THUMBNAILS = 'thumbnails' - class FileSystem: _instance = None @@ -54,8 +52,19 @@ class FileSystem: return path if cache_type else self._cache_dir + def get_cache_file_dir(self, filename, cache_type=None): + path = join(self.get_cache_dir(cache_type), filename[:2]) + if not isdir(path): + try: + makedirs(path) + except OSError: + self.log.info(f'Failed to create path {path} (Permission denied).') + return False + + return path + 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_file_dir(filename, cache_type), filename) if filename else None def get_cache_file_exists(self, filename, cache_type=None): path = self.get_cache_file_path(filename, cache_type) @@ -78,7 +87,7 @@ class FileSystem: return False def delete_cache_file(self, filename, cache_type=None): - path = join(self.get_cache_dir(cache_type), filename) + path = self.get_cache_file_path(filename, cache_type) if isfile(path): try: remove(path) diff --git a/cps/helper.py b/cps/helper.py index 1652be23..2f2df0e0 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -55,7 +55,7 @@ from . import calibre_db from .tasks.convert import TaskConvert from . import logger, config, get_locale, db, fs, ub from . import gdriveutils as gd -from .constants import STATIC_DIR as _STATIC_DIR +from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES from .subproc_wrapper import process_wait from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .tasks.mail import TaskEmail @@ -575,8 +575,9 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) thumbnail = get_book_cover_thumbnail(book, resolution) if thumbnail: cache = fs.FileSystem() - 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) + if cache.get_cache_file_exists(thumbnail.filename, CACHE_TYPE_THUMBNAILS): + return send_from_directory(cache.get_cache_file_dir(thumbnail.filename, CACHE_TYPE_THUMBNAILS), + thumbnail.filename) # Send the book cover from Google Drive if configured if config.config_use_google_drive: @@ -606,14 +607,54 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) def get_book_cover_thumbnail(book, resolution): if book and book.has_cover: - return ub.session\ - .query(ub.Thumbnail)\ - .filter(ub.Thumbnail.book_id == book.id)\ - .filter(ub.Thumbnail.resolution == resolution)\ - .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\ + return ub.session \ + .query(ub.Thumbnail) \ + .filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER) \ + .filter(ub.Thumbnail.entity_id == book.id) \ + .filter(ub.Thumbnail.resolution == resolution) \ + .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .first() +def get_series_thumbnail_on_failure(series_id, resolution): + book = calibre_db.session \ + .query(db.Books) \ + .join(db.books_series_link) \ + .join(db.Series) \ + .filter(db.Series.id == series_id) \ + .filter(db.Books.has_cover == 1) \ + .first() + + return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution) + + +def get_series_cover_thumbnail(series_id, resolution=None): + return get_series_cover_internal(series_id, resolution) + + +def get_series_cover_internal(series_id, resolution=None): + # Send the series thumbnail if it exists in cache + if resolution: + thumbnail = get_series_thumbnail(series_id, resolution) + if thumbnail: + cache = fs.FileSystem() + if cache.get_cache_file_exists(thumbnail.filename, CACHE_TYPE_THUMBNAILS): + return send_from_directory(cache.get_cache_file_dir(thumbnail.filename, CACHE_TYPE_THUMBNAILS), + thumbnail.filename) + + return get_series_thumbnail_on_failure(series_id, resolution) + + +def get_series_thumbnail(series_id, resolution): + return ub.session \ + .query(ub.Thumbnail) \ + .filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES) \ + .filter(ub.Thumbnail.entity_id == series_id) \ + .filter(ub.Thumbnail.resolution == resolution) \ + .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ + .first() + + # saves book cover from url def save_cover_from_url(url, book_path): try: diff --git a/cps/jinjia.py b/cps/jinjia.py index 8c8b72a9..1a58416a 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -32,9 +32,7 @@ from flask import Blueprint, request, url_for from flask_babel import get_locale from flask_login import current_user from markupsafe import escape -from . import logger -from .tasks.thumbnail import THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X - +from . import constants, logger jinjia = Blueprint('jinjia', __name__) log = logger.create() @@ -141,17 +139,44 @@ def uuidfilter(var): return uuid4() +@jinjia.app_template_filter('cache_timestamp') +def cache_timestamp(rolling_period='month'): + if rolling_period == 'day': + return str(int(datetime.datetime.today().replace(hour=1, minute=1).timestamp())) + elif rolling_period == 'year': + return str(int(datetime.datetime.today().replace(day=1).timestamp())) + else: + return str(int(datetime.datetime.today().replace(month=1, day=1).timestamp())) + + @jinjia.app_template_filter('last_modified') -def book_cover_cache_id(book): - timestamp = int(book.last_modified.timestamp() * 1000) - return str(timestamp) +def book_last_modified(book): + return str(int(book.last_modified.timestamp())) @jinjia.app_template_filter('get_cover_srcset') def get_cover_srcset(book): srcset = list() - for resolution in [THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X]: - timestamp = int(book.last_modified.timestamp() * 1000) - url = url_for('web.get_cover', book_id=book.id, resolution=resolution, cache_bust=str(timestamp)) + resolutions = { + constants.COVER_THUMBNAIL_SMALL: 'sm', + constants.COVER_THUMBNAIL_MEDIUM: 'md', + constants.COVER_THUMBNAIL_LARGE: 'lg' + } + for resolution, shortname in resolutions.items(): + url = url_for('web.get_cover', book_id=book.id, resolution=shortname, c=book_last_modified(book)) + srcset.append(f'{url} {resolution}x') + return ', '.join(srcset) + + +@jinjia.app_template_filter('get_series_srcset') +def get_cover_srcset(series): + srcset = list() + resolutions = { + constants.COVER_THUMBNAIL_SMALL: 'sm', + constants.COVER_THUMBNAIL_MEDIUM: 'md', + constants.COVER_THUMBNAIL_LARGE: 'lg' + } + for resolution, shortname in resolutions.items(): + url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp()) srcset.append(f'{url} {resolution}x') return ', '.join(srcset) diff --git a/cps/schedule.py b/cps/schedule.py index 8c350bd5..dc153b9a 100644 --- a/cps/schedule.py +++ b/cps/schedule.py @@ -21,7 +21,7 @@ from __future__ import division, print_function, unicode_literals from .services.background_scheduler import BackgroundScheduler from .services.worker import WorkerThread from .tasks.database import TaskReconnectDatabase -from .tasks.thumbnail import TaskGenerateCoverThumbnails +from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails def register_jobs(): @@ -37,3 +37,4 @@ def register_jobs(): def register_startup_jobs(): WorkerThread.add(None, TaskGenerateCoverThumbnails()) + # WorkerThread.add(None, TaskGenerateSeriesThumbnails()) diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py index 688f6e4b..152b8772 100644 --- a/cps/tasks/thumbnail.py +++ b/cps/tasks/thumbnail.py @@ -19,10 +19,11 @@ from __future__ import division, print_function, unicode_literals import os +from .. import constants from cps import config, db, fs, gdriveutils, logger, ub from cps.services.worker import CalibreTask from datetime import datetime, timedelta -from sqlalchemy import or_ +from sqlalchemy import func, text, or_ try: from urllib.request import urlopen @@ -35,9 +36,34 @@ try: except (ImportError, RuntimeError) as e: use_IM = False -THUMBNAIL_RESOLUTION_1X = 1 -THUMBNAIL_RESOLUTION_2X = 2 -THUMBNAIL_RESOLUTION_3X = 3 + +def get_resize_height(resolution): + return int(225 * resolution) + + +def get_resize_width(resolution, original_width, original_height): + height = get_resize_height(resolution) + percent = (height / float(original_height)) + width = int((float(original_width) * float(percent))) + return width if width % 2 == 0 else width + 1 + + +def get_best_fit(width, height, image_width, image_height): + resize_width = int(width / 2.0) + resize_height = int(height / 2.0) + aspect_ratio = image_width / image_height + + # If this image's aspect ratio is different than the first image, then resize this image + # to fill the width and height of the first image + if aspect_ratio < width / height: + resize_width = int(width / 2.0) + resize_height = image_height * int(width / 2.0) / image_width + + elif aspect_ratio > width / height: + resize_width = image_width * int(height / 2.0) / image_height + resize_height = int(height / 2.0) + + return {'width': resize_width, 'height': resize_height} class TaskGenerateCoverThumbnails(CalibreTask): @@ -48,8 +74,8 @@ class TaskGenerateCoverThumbnails(CalibreTask): self.calibre_db = db.CalibreDB(expire_on_commit=False) self.cache = fs.FileSystem() self.resolutions = [ - THUMBNAIL_RESOLUTION_1X, - THUMBNAIL_RESOLUTION_2X + constants.COVER_THUMBNAIL_SMALL, + constants.COVER_THUMBNAIL_MEDIUM ] def run(self, worker_thread): @@ -75,7 +101,7 @@ class TaskGenerateCoverThumbnails(CalibreTask): updated += 1 self.update_book_cover_thumbnail(book, thumbnail) - elif not self.cache.get_cache_file_exists(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS): + elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS): updated += 1 self.update_book_cover_thumbnail(book, thumbnail) @@ -86,21 +112,23 @@ class TaskGenerateCoverThumbnails(CalibreTask): self.app_db_session.remove() def get_books_with_covers(self): - return self.calibre_db.session\ - .query(db.Books)\ - .filter(db.Books.has_cover == 1)\ + return self.calibre_db.session \ + .query(db.Books) \ + .filter(db.Books.has_cover == 1) \ .all() def get_book_cover_thumbnails(self, book_id): - return self.app_db_session\ - .query(ub.Thumbnail)\ - .filter(ub.Thumbnail.book_id == book_id)\ - .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\ + return self.app_db_session \ + .query(ub.Thumbnail) \ + .filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \ + .filter(ub.Thumbnail.entity_id == book_id) \ + .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .all() def create_book_cover_thumbnail(self, book, resolution): thumbnail = ub.Thumbnail() - thumbnail.book_id = book.id + thumbnail.type = constants.THUMBNAIL_TYPE_COVER + thumbnail.entity_id = book.id thumbnail.format = 'jpeg' thumbnail.resolution = resolution @@ -118,7 +146,7 @@ class TaskGenerateCoverThumbnails(CalibreTask): try: self.app_db_session.commit() - self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS) + self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) self.generate_book_thumbnail(book, thumbnail) except Exception as ex: self.log.info(u'Error updating book thumbnail: ' + str(ex)) @@ -144,7 +172,8 @@ class TaskGenerateCoverThumbnails(CalibreTask): width = self.get_thumbnail_width(height, img) img.resize(width=width, height=height, filter='lanczos') img.format = thumbnail.format - filename = self.cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS) + filename = self.cache.get_cache_file_path(thumbnail.filename, + constants.CACHE_TYPE_THUMBNAILS) img.save(filename=filename) except Exception as ex: # Bubble exception to calling function @@ -158,24 +187,210 @@ class TaskGenerateCoverThumbnails(CalibreTask): raise Exception('Book cover file not found') with Image(filename=book_cover_filepath) as img: - height = self.get_thumbnail_height(thumbnail) + height = get_resize_height(thumbnail.resolution) if img.height > height: - width = self.get_thumbnail_width(height, img) + width = get_resize_width(thumbnail.resolution, img.width, img.height) img.resize(width=width, height=height, filter='lanczos') img.format = thumbnail.format - filename = self.cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS) + filename = self.cache.get_cache_file_path(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) img.save(filename=filename) - def get_thumbnail_height(self, thumbnail): - return int(225 * thumbnail.resolution) + @property + def name(self): + return "ThumbnailsGenerate" + + +class TaskGenerateSeriesThumbnails(CalibreTask): + def __init__(self, task_message=u'Generating series thumbnails'): + super(TaskGenerateSeriesThumbnails, 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() + self.resolutions = [ + constants.COVER_THUMBNAIL_SMALL, + constants.COVER_THUMBNAIL_MEDIUM + ] + + # get all series + # get all books in series with covers and count >= 4 books + # get the dimensions from the first book in the series & pop the first book from the series list of books + # randomly select three other books in the series + + # resize the covers in the sequence? + # create an image sequence from the 4 selected books of the series + # join pairs of books in the series with wand's concat + # join the two sets of pairs with wand's + + def run(self, worker_thread): + if self.calibre_db.session and use_IM: + all_series = self.get_series_with_four_plus_books() + count = len(all_series) + + updated = 0 + generated = 0 + for i, series in enumerate(all_series): + series_thumbnails = self.get_series_thumbnails(series.id) + series_books = self.get_series_books(series.id) + + # Generate new thumbnails for missing covers + resolutions = list(map(lambda t: t.resolution, series_thumbnails)) + missing_resolutions = list(set(self.resolutions).difference(resolutions)) + for resolution in missing_resolutions: + generated += 1 + self.create_series_thumbnail(series, series_books, resolution) + + # Replace outdated or missing thumbnails + for thumbnail in series_thumbnails: + if any(book.last_modified > thumbnail.generated_at for book in series_books): + updated += 1 + self.update_series_thumbnail(series_books, thumbnail) + + elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS): + updated += 1 + self.update_series_thumbnail(series_books, thumbnail) + + self.message = u'Processing series {0} of {1}'.format(i + 1, count) + self.progress = (1.0 / count) * i + + self._handleSuccess() + self.app_db_session.remove() + + def get_series_with_four_plus_books(self): + return self.calibre_db.session \ + .query(db.Series) \ + .join(db.books_series_link) \ + .join(db.Books) \ + .filter(db.Books.has_cover == 1) \ + .group_by(text('books_series_link.series')) \ + .having(func.count('book_series_link') > 3) \ + .all() + + def get_series_books(self, series_id): + return self.calibre_db.session \ + .query(db.Books) \ + .join(db.books_series_link) \ + .join(db.Series) \ + .filter(db.Books.has_cover == 1) \ + .filter(db.Series.id == series_id) \ + .all() + + def get_series_thumbnails(self, series_id): + return self.app_db_session \ + .query(ub.Thumbnail) \ + .filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES) \ + .filter(ub.Thumbnail.entity_id == series_id) \ + .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ + .all() + + def create_series_thumbnail(self, series, series_books, resolution): + thumbnail = ub.Thumbnail() + thumbnail.type = constants.THUMBNAIL_TYPE_SERIES + thumbnail.entity_id = series.id + thumbnail.format = 'jpeg' + thumbnail.resolution = resolution + + self.app_db_session.add(thumbnail) + try: + self.app_db_session.commit() + self.generate_series_thumbnail(series_books, thumbnail) + except Exception as ex: + self.log.info(u'Error creating book thumbnail: ' + str(ex)) + self._handleError(u'Error creating book thumbnail: ' + str(ex)) + self.app_db_session.rollback() - def get_thumbnail_width(self, height, img): - percent = (height / float(img.height)) - return int((float(img.width) * float(percent))) + def update_series_thumbnail(self, series_books, thumbnail): + thumbnail.generated_at = datetime.utcnow() + + try: + self.app_db_session.commit() + self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) + self.generate_series_thumbnail(series_books, 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_series_thumbnail(self, series_books, thumbnail): + books = series_books[:4] + + top = 0 + left = 0 + width = 0 + height = 0 + with Image() as canvas: + for book in books: + if config.config_use_google_drive: + if not gdriveutils.is_gdrive_ready(): + raise Exception('Google Drive is configured but not ready') + + web_content_link = gdriveutils.get_cover_via_gdrive(book.path) + if not web_content_link: + raise Exception('Google Drive cover url not found') + + stream = None + try: + stream = urlopen(web_content_link) + with Image(file=stream) as img: + # Use the first image in this set to determine the width and height to scale the + # other images in this set + if width == 0 or height == 0: + width = get_resize_width(thumbnail.resolution, img.width, img.height) + height = get_resize_height(thumbnail.resolution) + canvas.blank(width, height) + + dimensions = get_best_fit(width, height, img.width, img.height) + + # resize and crop the image + img.resize(width=int(dimensions['width']), height=int(dimensions['height']), filter='lanczos') + img.crop(width=int(width / 2.0), height=int(height / 2.0), gravity='center') + + # add the image to the canvas + canvas.composite(img, left, top) + + except Exception as ex: + self.log.info(u'Error generating thumbnail file: ' + str(ex)) + raise ex + finally: + stream.close() + + book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') + if not os.path.isfile(book_cover_filepath): + raise Exception('Book cover file not found') + + with Image(filename=book_cover_filepath) as img: + # Use the first image in this set to determine the width and height to scale the + # other images in this set + if width == 0 or height == 0: + width = get_resize_width(thumbnail.resolution, img.width, img.height) + height = get_resize_height(thumbnail.resolution) + canvas.blank(width, height) + + dimensions = get_best_fit(width, height, img.width, img.height) + + # resize and crop the image + img.resize(width=int(dimensions['width']), height=int(dimensions['height']), filter='lanczos') + img.crop(width=int(width / 2.0), height=int(height / 2.0), gravity='center') + + # add the image to the canvas + canvas.composite(img, left, top) + + # set the coordinates for the next iteration + if left == 0 and top == 0: + left = int(width / 2.0) + elif left == int(width / 2.0) and top == 0: + left = 0 + top = int(height / 2.0) + else: + left = int(width / 2.0) + + canvas.format = thumbnail.format + filename = self.cache.get_cache_file_path(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) + canvas.save(filename=filename) @property def name(self): - return "ThumbnailsGenerate" + return "SeriesThumbnailGenerate" class TaskClearCoverThumbnailCache(CalibreTask): @@ -199,21 +414,22 @@ class TaskClearCoverThumbnailCache(CalibreTask): 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)\ + return self.app_db_session \ + .query(ub.Thumbnail) \ + .filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \ + .filter(ub.Thumbnail.entity_id == book_id) \ .all() def delete_thumbnail(self, thumbnail): try: - self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS) + self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS) except Exception as ex: self.log.info(u'Error deleting book thumbnail: ' + str(ex)) self._handleError(u'Error deleting book thumbnail: ' + str(ex)) def delete_all_thumbnails(self): try: - self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS) + self.cache.delete_cache_dir(constants.CACHE_TYPE_THUMBNAILS) except Exception as ex: self.log.info(u'Error deleting book thumbnails: ' + str(ex)) self._handleError(u'Error deleting book thumbnails: ' + str(ex)) diff --git a/cps/templates/author.html b/cps/templates/author.html index 2ff1ce7a..df891d5b 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -37,7 +37,7 @@
- {{ book_cover_image(entry, title=author.name|safe) }} + {{ image.book_cover(entry, title=author.name|safe) }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/book_cover.html b/cps/templates/book_cover.html deleted file mode 100644 index 7d3a84f4..00000000 --- a/cps/templates/book_cover.html +++ /dev/null @@ -1,11 +0,0 @@ -{% macro book_cover_image(book, title=None) -%} - {%- set book_title = book.title if book.title else book.name -%} - {%- set book_title = title if title else book_title -%} - {% set srcset = book|get_cover_srcset %} - {{ book_title }} -{%- endmacro %} diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 8525b8e3..b5f5df7a 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -1,10 +1,10 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %} {% if book %}
- {{ book_cover_image(book) }} + {{ image.book_cover(book) }}
{% if g.user.role_delete_books() %}
diff --git a/cps/templates/detail.html b/cps/templates/detail.html index b538ff9a..7985691a 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -4,7 +4,7 @@
- {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }}
diff --git a/cps/templates/discover.html b/cps/templates/discover.html index 01cd393d..d697ff0a 100644 --- a/cps/templates/discover.html +++ b/cps/templates/discover.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %}
@@ -10,7 +10,7 @@ {% if entry.has_cover is defined %} - {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/fragment.html b/cps/templates/fragment.html index 901dd193..f2e94fb2 100644 --- a/cps/templates/fragment.html +++ b/cps/templates/fragment.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %}
{% block body %}{% endblock %}
diff --git a/cps/templates/grid.html b/cps/templates/grid.html index 45fb53bf..b3395fcb 100644 --- a/cps/templates/grid.html +++ b/cps/templates/grid.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %}

{{_(title)}}

@@ -29,7 +29,7 @@
- {{ book_cover_image(entry[0], title=entry[0].series[0].name|shortentitle) }} + {{ image.series(entry[0].series[0], title=entry[0].series[0].name|shortentitle) }} {{entry.count}} diff --git a/cps/templates/image.html b/cps/templates/image.html new file mode 100644 index 00000000..ae57727b --- /dev/null +++ b/cps/templates/image.html @@ -0,0 +1,23 @@ +{% macro book_cover(book, title=None, alt=None) -%} + {%- set image_title = book.title if book.title else book.name -%} + {%- set image_title = title if title else image_title -%} + {%- set image_alt = alt if alt else image_title -%} + {% set srcset = book|get_cover_srcset %} + {{ image_alt }} +{%- endmacro %} + +{% macro series(series, title=None, alt=None) -%} + {%- set image_alt = alt if alt else image_title -%} + {% set srcset = series|get_series_srcset %} + {{ book_title }} +{%- endmacro %} diff --git a/cps/templates/index.html b/cps/templates/index.html index e47c6c70..709d6ce7 100644 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %} {% if g.user.show_detail_random() %} @@ -10,7 +10,7 @@
- {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }} {% if entry.id in read_book_ids %}{% endif %} @@ -87,7 +87,7 @@
- {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 63dc7469..4a664ec1 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -1,5 +1,5 @@ {% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal, change_confirm_modal %} -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} diff --git a/cps/templates/search.html b/cps/templates/search.html index 872b6dc1..15ff344f 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %}
@@ -45,7 +45,7 @@ {% if entry.has_cover is defined %} - {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 0e5865c9..18dbde8b 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -1,4 +1,4 @@ -{% from 'book_cover.html' import book_cover_image %} +{% import 'image.html' as image %} {% extends "layout.html" %} {% block body %}
@@ -32,7 +32,7 @@
- {{ book_cover_image(entry) }} + {{ image.book_cover(entry) }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/ub.py b/cps/ub.py index f108a1ce..7666f0f2 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -526,10 +526,11 @@ class Thumbnail(Base): __tablename__ = 'thumbnail' id = Column(Integer, primary_key=True) - book_id = Column(Integer) + entity_id = Column(Integer) uuid = Column(String, default=lambda: str(uuid.uuid4()), unique=True) format = Column(String, default='jpeg') - resolution = Column(SmallInteger, default=1) + type = Column(SmallInteger, default=constants.THUMBNAIL_TYPE_COVER) + resolution = Column(SmallInteger, default=constants.COVER_THUMBNAIL_SMALL) filename = Column(String, default=filename) generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow()) expiration = Column(DateTime, nullable=True) diff --git a/cps/web.py b/cps/web.py index 8f78ff32..88a5c0ab 100644 --- a/cps/web.py +++ b/cps/web.py @@ -50,8 +50,8 @@ from . import constants, logger, isoLanguages, services from . import babel, db, ub, config, get_locale, app from . import calibre_db from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download -from .helper import check_valid_domain, render_task_status, check_email, check_username, \ - get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ +from .helper import check_valid_domain, render_task_status, check_email, check_username, get_cc_columns, \ + get_book_cover, get_series_cover_thumbnail, get_download_link, send_mail, generate_random_password, \ send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email from .pagination import Pagination from .redirect import redirect_back @@ -1388,11 +1388,31 @@ def advanced_search_form(): @web.route("/cover/") -@web.route("/cover//") -@web.route("/cover///") +@web.route("/cover//") @login_required_if_no_ano -def get_cover(book_id, resolution=None, cache_bust=None): - return get_book_cover(book_id, resolution) +def get_cover(book_id, resolution=None): + resolutions = { + 'og': constants.COVER_THUMBNAIL_ORIGINAL, + 'sm': constants.COVER_THUMBNAIL_SMALL, + 'md': constants.COVER_THUMBNAIL_MEDIUM, + 'lg': constants.COVER_THUMBNAIL_LARGE, + } + cover_resolution = resolutions.get(resolution, None) + return get_book_cover(book_id, cover_resolution) + + +@web.route("/series_cover/") +@web.route("/series_cover//") +@login_required_if_no_ano +def get_series_cover(series_id, resolution=None): + resolutions = { + 'og': constants.COVER_THUMBNAIL_ORIGINAL, + 'sm': constants.COVER_THUMBNAIL_SMALL, + 'md': constants.COVER_THUMBNAIL_MEDIUM, + 'lg': constants.COVER_THUMBNAIL_LARGE, + } + cover_resolution = resolutions.get(resolution, None) + return get_series_cover_thumbnail(series_id, cover_resolution) @web.route("/robots.txt")