Added series cover thumbnail generation. Better cache file handling.

This commit is contained in:
mmonkey 2021-09-25 03:04:38 -05:00
parent be28a91315
commit 0bd544704d
21 changed files with 430 additions and 92 deletions

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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())

View File

@ -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,26 +187,212 @@ 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)
def get_thumbnail_width(self, height, img):
percent = (height / float(img.height))
return int((float(img.width) * float(percent)))
@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 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 "SeriesThumbnailGenerate"
class TaskClearCoverThumbnailCache(CalibreTask):
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
@ -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))

View File

@ -37,7 +37,7 @@
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img">
{{ book_cover_image(entry, title=author.name|safe) }}
{{ image.book_cover(entry, title=author.name|safe) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>

View File

@ -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 %}
<img
srcset="{{ srcset }}"
src="{{ url_for('web.get_cover', book_id=book.id, resolution=0, cache_bust=book|last_modified) }}"
title="{{ book_title }}"
alt="{{ book_title }}"
/>
{%- endmacro %}

View File

@ -1,10 +1,10 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
{% if book %}
<div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover">
{{ book_cover_image(book) }}
{{ image.book_cover(book) }}
</div>
{% if g.user.role_delete_books() %}
<div class="text-center">

View File

@ -4,7 +4,7 @@
<div class="row">
<div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
</div>
</div>
<div class="col-sm-9 col-lg-9 book-meta">

View File

@ -1,4 +1,4 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
<div class="discover load-more">
@ -10,7 +10,7 @@
{% 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">
<span class="img">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>

View File

@ -1,4 +1,4 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
<div class="container-fluid">
{% block body %}{% endblock %}
</div>

View File

@ -1,4 +1,4 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
<h1 class="{{page}}">{{_(title)}}</h1>
@ -29,7 +29,7 @@
<div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
<span class="img">
{{ 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) }}
<span class="badge">{{entry.count}}</span>
</span>
</a>

23
cps/templates/image.html Normal file
View File

@ -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 %}
<img
srcset="{{ srcset }}"
src="{{ url_for('web.get_cover', book_id=book.id, resolution='og', c=book|last_modified) }}"
title="{{ image_title }}"
alt="{{ 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 %}
<img
srcset="{{ srcset }}"
src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='month'|cache_timestamp) }}"
title="{{ title }}"
alt="{{ book_title }}"
/>
{%- endmacro %}

View File

@ -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 @@
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
@ -87,7 +87,7 @@
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>

View File

@ -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 %}
<!DOCTYPE html>
<html lang="{{ g.user.locale }}">
<head>

View File

@ -1,4 +1,4 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
<div class="discover">
@ -45,7 +45,7 @@
{% 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">
<span class="img">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>

View File

@ -1,4 +1,4 @@
{% from 'book_cover.html' import book_cover_image %}
{% import 'image.html' as image %}
{% extends "layout.html" %}
{% block body %}
<div class="discover">
@ -32,7 +32,7 @@
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img">
{{ book_cover_image(entry) }}
{{ image.book_cover(entry) }}
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>

View File

@ -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)

View File

@ -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/<int:book_id>")
@web.route("/cover/<int:book_id>/<int:resolution>")
@web.route("/cover/<int:book_id>/<int:resolution>/<string:cache_bust>")
@web.route("/cover/<int:book_id>/<string:resolution>")
@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/<int:series_id>")
@web.route("/series_cover/<int:series_id>/<string:resolution>")
@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")