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())
|
cache_type = request.args.get('cache_type'.strip())
|
||||||
showtext = {}
|
showtext = {}
|
||||||
|
|
||||||
if cache_type == fs.CACHE_TYPE_THUMBNAILS:
|
if cache_type == constants.CACHE_TYPE_THUMBNAILS:
|
||||||
log.info('clearing cover thumbnail cache')
|
log.info('clearing cover thumbnail cache')
|
||||||
showtext['text'] = _(u'Cleared cover thumbnail cache')
|
showtext['text'] = _(u'Cleared cover thumbnail cache')
|
||||||
helper.clear_cover_thumbnail_cache()
|
helper.clear_cover_thumbnail_cache()
|
||||||
|
|
|
@ -169,6 +169,19 @@ NIGHTLY_VERSION[1] = '$Format:%cI$'
|
||||||
# NIGHTLY_VERSION[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
|
# NIGHTLY_VERSION[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
|
||||||
# NIGHTLY_VERSION[1] = '2018-09-09T10:13:08+02:00'
|
# 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
|
# clean-up the module namespace
|
||||||
del sys, os, namedtuple
|
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 __future__ import division, print_function, unicode_literals
|
||||||
from . import logger
|
from . import logger
|
||||||
from .constants import CACHE_DIR
|
from .constants import CACHE_DIR
|
||||||
from os import listdir, makedirs, remove
|
from os import makedirs, remove
|
||||||
from os.path import isdir, isfile, join
|
from os.path import isdir, isfile, join
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
|
||||||
CACHE_TYPE_THUMBNAILS = 'thumbnails'
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystem:
|
class FileSystem:
|
||||||
_instance = None
|
_instance = None
|
||||||
|
@ -54,8 +52,19 @@ class FileSystem:
|
||||||
|
|
||||||
return path if cache_type else self._cache_dir
|
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):
|
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):
|
def get_cache_file_exists(self, filename, cache_type=None):
|
||||||
path = self.get_cache_file_path(filename, cache_type)
|
path = self.get_cache_file_path(filename, cache_type)
|
||||||
|
@ -78,7 +87,7 @@ class FileSystem:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_cache_file(self, filename, cache_type=None):
|
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):
|
if isfile(path):
|
||||||
try:
|
try:
|
||||||
remove(path)
|
remove(path)
|
||||||
|
|
|
@ -55,7 +55,7 @@ from . import calibre_db
|
||||||
from .tasks.convert import TaskConvert
|
from .tasks.convert import TaskConvert
|
||||||
from . import logger, config, get_locale, db, fs, ub
|
from . import logger, config, get_locale, db, fs, ub
|
||||||
from . import gdriveutils as gd
|
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 .subproc_wrapper import process_wait
|
||||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
||||||
from .tasks.mail import TaskEmail
|
from .tasks.mail import TaskEmail
|
||||||
|
@ -575,8 +575,9 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
|
||||||
thumbnail = get_book_cover_thumbnail(book, resolution)
|
thumbnail = get_book_cover_thumbnail(book, resolution)
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
cache = fs.FileSystem()
|
cache = fs.FileSystem()
|
||||||
if cache.get_cache_file_exists(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
|
if cache.get_cache_file_exists(thumbnail.filename, CACHE_TYPE_THUMBNAILS):
|
||||||
return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename)
|
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
|
# Send the book cover from Google Drive if configured
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
|
@ -606,11 +607,51 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None)
|
||||||
|
|
||||||
def get_book_cover_thumbnail(book, resolution):
|
def get_book_cover_thumbnail(book, resolution):
|
||||||
if book and book.has_cover:
|
if book and book.has_cover:
|
||||||
return ub.session\
|
return ub.session \
|
||||||
.query(ub.Thumbnail)\
|
.query(ub.Thumbnail) \
|
||||||
.filter(ub.Thumbnail.book_id == book.id)\
|
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER) \
|
||||||
.filter(ub.Thumbnail.resolution == resolution)\
|
.filter(ub.Thumbnail.entity_id == book.id) \
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\
|
.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()
|
.first()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,7 @@ from flask import Blueprint, request, url_for
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
from . import logger
|
from . import constants, logger
|
||||||
from .tasks.thumbnail import THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X
|
|
||||||
|
|
||||||
|
|
||||||
jinjia = Blueprint('jinjia', __name__)
|
jinjia = Blueprint('jinjia', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
@ -141,17 +139,44 @@ def uuidfilter(var):
|
||||||
return uuid4()
|
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')
|
@jinjia.app_template_filter('last_modified')
|
||||||
def book_cover_cache_id(book):
|
def book_last_modified(book):
|
||||||
timestamp = int(book.last_modified.timestamp() * 1000)
|
return str(int(book.last_modified.timestamp()))
|
||||||
return str(timestamp)
|
|
||||||
|
|
||||||
|
|
||||||
@jinjia.app_template_filter('get_cover_srcset')
|
@jinjia.app_template_filter('get_cover_srcset')
|
||||||
def get_cover_srcset(book):
|
def get_cover_srcset(book):
|
||||||
srcset = list()
|
srcset = list()
|
||||||
for resolution in [THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X, THUMBNAIL_RESOLUTION_3X]:
|
resolutions = {
|
||||||
timestamp = int(book.last_modified.timestamp() * 1000)
|
constants.COVER_THUMBNAIL_SMALL: 'sm',
|
||||||
url = url_for('web.get_cover', book_id=book.id, resolution=resolution, cache_bust=str(timestamp))
|
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')
|
srcset.append(f'{url} {resolution}x')
|
||||||
return ', '.join(srcset)
|
return ', '.join(srcset)
|
||||||
|
|
|
@ -21,7 +21,7 @@ from __future__ import division, print_function, unicode_literals
|
||||||
from .services.background_scheduler import BackgroundScheduler
|
from .services.background_scheduler import BackgroundScheduler
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.database import TaskReconnectDatabase
|
from .tasks.database import TaskReconnectDatabase
|
||||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails
|
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails
|
||||||
|
|
||||||
|
|
||||||
def register_jobs():
|
def register_jobs():
|
||||||
|
@ -37,3 +37,4 @@ def register_jobs():
|
||||||
|
|
||||||
def register_startup_jobs():
|
def register_startup_jobs():
|
||||||
WorkerThread.add(None, TaskGenerateCoverThumbnails())
|
WorkerThread.add(None, TaskGenerateCoverThumbnails())
|
||||||
|
# WorkerThread.add(None, TaskGenerateSeriesThumbnails())
|
||||||
|
|
|
@ -19,10 +19,11 @@
|
||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from .. import constants
|
||||||
from cps import config, db, fs, gdriveutils, logger, ub
|
from cps import config, db, fs, gdriveutils, logger, ub
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import func, text, or_
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
@ -35,9 +36,34 @@ try:
|
||||||
except (ImportError, RuntimeError) as e:
|
except (ImportError, RuntimeError) as e:
|
||||||
use_IM = False
|
use_IM = False
|
||||||
|
|
||||||
THUMBNAIL_RESOLUTION_1X = 1
|
|
||||||
THUMBNAIL_RESOLUTION_2X = 2
|
def get_resize_height(resolution):
|
||||||
THUMBNAIL_RESOLUTION_3X = 3
|
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):
|
class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
|
@ -48,8 +74,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
self.calibre_db = db.CalibreDB(expire_on_commit=False)
|
self.calibre_db = db.CalibreDB(expire_on_commit=False)
|
||||||
self.cache = fs.FileSystem()
|
self.cache = fs.FileSystem()
|
||||||
self.resolutions = [
|
self.resolutions = [
|
||||||
THUMBNAIL_RESOLUTION_1X,
|
constants.COVER_THUMBNAIL_SMALL,
|
||||||
THUMBNAIL_RESOLUTION_2X
|
constants.COVER_THUMBNAIL_MEDIUM
|
||||||
]
|
]
|
||||||
|
|
||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
|
@ -75,7 +101,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
updated += 1
|
updated += 1
|
||||||
self.update_book_cover_thumbnail(book, thumbnail)
|
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
|
updated += 1
|
||||||
self.update_book_cover_thumbnail(book, thumbnail)
|
self.update_book_cover_thumbnail(book, thumbnail)
|
||||||
|
|
||||||
|
@ -86,21 +112,23 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
self.app_db_session.remove()
|
self.app_db_session.remove()
|
||||||
|
|
||||||
def get_books_with_covers(self):
|
def get_books_with_covers(self):
|
||||||
return self.calibre_db.session\
|
return self.calibre_db.session \
|
||||||
.query(db.Books)\
|
.query(db.Books) \
|
||||||
.filter(db.Books.has_cover == 1)\
|
.filter(db.Books.has_cover == 1) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def get_book_cover_thumbnails(self, book_id):
|
def get_book_cover_thumbnails(self, book_id):
|
||||||
return self.app_db_session\
|
return self.app_db_session \
|
||||||
.query(ub.Thumbnail)\
|
.query(ub.Thumbnail) \
|
||||||
.filter(ub.Thumbnail.book_id == book_id)\
|
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
|
||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow()))\
|
.filter(ub.Thumbnail.entity_id == book_id) \
|
||||||
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def create_book_cover_thumbnail(self, book, resolution):
|
def create_book_cover_thumbnail(self, book, resolution):
|
||||||
thumbnail = ub.Thumbnail()
|
thumbnail = ub.Thumbnail()
|
||||||
thumbnail.book_id = book.id
|
thumbnail.type = constants.THUMBNAIL_TYPE_COVER
|
||||||
|
thumbnail.entity_id = book.id
|
||||||
thumbnail.format = 'jpeg'
|
thumbnail.format = 'jpeg'
|
||||||
thumbnail.resolution = resolution
|
thumbnail.resolution = resolution
|
||||||
|
|
||||||
|
@ -118,7 +146,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.app_db_session.commit()
|
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)
|
self.generate_book_thumbnail(book, thumbnail)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error updating book thumbnail: ' + str(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)
|
width = self.get_thumbnail_width(height, img)
|
||||||
img.resize(width=width, height=height, filter='lanczos')
|
img.resize(width=width, height=height, filter='lanczos')
|
||||||
img.format = thumbnail.format
|
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)
|
img.save(filename=filename)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
# Bubble exception to calling function
|
# Bubble exception to calling function
|
||||||
|
@ -158,26 +187,212 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
raise Exception('Book cover file not found')
|
raise Exception('Book cover file not found')
|
||||||
|
|
||||||
with Image(filename=book_cover_filepath) as img:
|
with Image(filename=book_cover_filepath) as img:
|
||||||
height = self.get_thumbnail_height(thumbnail)
|
height = get_resize_height(thumbnail.resolution)
|
||||||
if img.height > height:
|
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.resize(width=width, height=height, filter='lanczos')
|
||||||
img.format = thumbnail.format
|
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)
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "ThumbnailsGenerate"
|
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):
|
class TaskClearCoverThumbnailCache(CalibreTask):
|
||||||
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
|
def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
|
||||||
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
|
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
|
||||||
|
@ -199,21 +414,22 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
||||||
self.app_db_session.remove()
|
self.app_db_session.remove()
|
||||||
|
|
||||||
def get_thumbnails_for_book(self, book_id):
|
def get_thumbnails_for_book(self, book_id):
|
||||||
return self.app_db_session\
|
return self.app_db_session \
|
||||||
.query(ub.Thumbnail)\
|
.query(ub.Thumbnail) \
|
||||||
.filter(ub.Thumbnail.book_id == book_id)\
|
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
|
||||||
|
.filter(ub.Thumbnail.entity_id == book_id) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def delete_thumbnail(self, thumbnail):
|
def delete_thumbnail(self, thumbnail):
|
||||||
try:
|
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:
|
except Exception as ex:
|
||||||
self.log.info(u'Error deleting book thumbnail: ' + str(ex))
|
self.log.info(u'Error deleting book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error deleting book thumbnail: ' + str(ex))
|
self._handleError(u'Error deleting book thumbnail: ' + str(ex))
|
||||||
|
|
||||||
def delete_all_thumbnails(self):
|
def delete_all_thumbnails(self):
|
||||||
try:
|
try:
|
||||||
self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS)
|
self.cache.delete_cache_dir(constants.CACHE_TYPE_THUMBNAILS)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error deleting book thumbnails: ' + str(ex))
|
self.log.info(u'Error deleting book thumbnails: ' + str(ex))
|
||||||
self._handleError(u'Error deleting book thumbnails: ' + str(ex))
|
self._handleError(u'Error deleting book thumbnails: ' + str(ex))
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry, 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 %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if book %}
|
{% if book %}
|
||||||
<div class="col-sm-3 col-lg-3 col-xs-12">
|
<div class="col-sm-3 col-lg-3 col-xs-12">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
{{ book_cover_image(book) }}
|
{{ image.book_cover(book) }}
|
||||||
</div>
|
</div>
|
||||||
{% if g.user.role_delete_books() %}
|
{% if g.user.role_delete_books() %}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 col-lg-3 col-xs-5">
|
<div class="col-sm-3 col-lg-3 col-xs-5">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-9 col-lg-9 book-meta">
|
<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" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover load-more">
|
<div class="discover load-more">
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
{% if entry.has_cover is defined %}
|
{% if entry.has_cover is defined %}
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% from 'book_cover.html' import book_cover_image %}
|
{% import 'image.html' as image %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% from 'book_cover.html' import book_cover_image %}
|
{% import 'image.html' as image %}
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
<h1 class="{{page}}">{{_(title)}}</h1>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
|
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry[0], 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 class="badge">{{entry.count}}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if g.user.show_detail_random() %}
|
{% if g.user.show_detail_random() %}
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal, change_confirm_modal %}
|
{% 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>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ g.user.locale }}">
|
<html lang="{{ g.user.locale }}">
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% from 'book_cover.html' import book_cover_image %}
|
{% import 'image.html' as image %}
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
{% if entry.has_cover is defined %}
|
{% if entry.has_cover is defined %}
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% from 'book_cover.html' import book_cover_image %}
|
{% import 'image.html' as image %}
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<span class="img">
|
<span class="img">
|
||||||
{{ book_cover_image(entry) }}
|
{{ image.book_cover(entry) }}
|
||||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -526,10 +526,11 @@ class Thumbnail(Base):
|
||||||
__tablename__ = 'thumbnail'
|
__tablename__ = 'thumbnail'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
book_id = Column(Integer)
|
entity_id = Column(Integer)
|
||||||
uuid = Column(String, default=lambda: str(uuid.uuid4()), unique=True)
|
uuid = Column(String, default=lambda: str(uuid.uuid4()), unique=True)
|
||||||
format = Column(String, default='jpeg')
|
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)
|
filename = Column(String, default=filename)
|
||||||
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
|
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
|
||||||
expiration = Column(DateTime, nullable=True)
|
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 babel, db, ub, config, get_locale, app
|
||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
||||||
from .helper import check_valid_domain, render_task_status, check_email, check_username, \
|
from .helper import check_valid_domain, render_task_status, check_email, check_username, get_cc_columns, \
|
||||||
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
|
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
|
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .redirect import redirect_back
|
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>")
|
||||||
@web.route("/cover/<int:book_id>/<int:resolution>")
|
@web.route("/cover/<int:book_id>/<string:resolution>")
|
||||||
@web.route("/cover/<int:book_id>/<int:resolution>/<string:cache_bust>")
|
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def get_cover(book_id, resolution=None, cache_bust=None):
|
def get_cover(book_id, resolution=None):
|
||||||
return get_book_cover(book_id, resolution)
|
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")
|
@web.route("/robots.txt")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user