mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-24 10:37:23 +00:00
Added series cover thumbnail generation. Better cache file handling.
This commit is contained in:
parent
be28a91315
commit
0bd544704d
@ -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()
|
||||
|
@ -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
|
||||
|
19
cps/fs.py
19
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)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
23
cps/templates/image.html
Normal 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 %}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
32
cps/web.py
32
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/<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")
|
||||
|
Loading…
Reference in New Issue
Block a user