Added thumbnail task and database table
This commit is contained in:
parent
9a20faf640
commit
774b9ae12d
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,6 +21,7 @@ vendor/
|
||||||
# calibre-web
|
# calibre-web
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
cps/cache
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
*.bak
|
*.bak
|
||||||
|
|
4
cps.py
4
cps.py
|
@ -43,6 +43,7 @@ from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
from cps.remotelogin import remotelogin
|
from cps.remotelogin import remotelogin
|
||||||
from cps.error_handler import init_errorhandler
|
from cps.error_handler import init_errorhandler
|
||||||
|
from cps.thumbnails import generate_thumbnails
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.kobo import kobo, get_kobo_activated
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
|
@ -78,6 +79,9 @@ def main():
|
||||||
app.register_blueprint(kobo_auth)
|
app.register_blueprint(kobo_auth)
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
|
|
||||||
|
generate_thumbnails()
|
||||||
|
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ else:
|
||||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||||
|
CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache')
|
||||||
|
|
||||||
if HOME_CONFIG:
|
if HOME_CONFIG:
|
||||||
home_dir = os.path.join(os.path.expanduser("~"),".calibre-web")
|
home_dir = os.path.join(os.path.expanduser("~"),".calibre-web")
|
||||||
|
|
|
@ -551,6 +551,11 @@ def get_book_cover_with_uuid(book_uuid,
|
||||||
|
|
||||||
def get_book_cover_internal(book, use_generic_cover_on_failure):
|
def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||||
if book and book.has_cover:
|
if book and book.has_cover:
|
||||||
|
# if thumbnails.cover_thumbnail_exists_for_book(book):
|
||||||
|
# thumbnail = ub.session.query(ub.Thumbnail).filter(ub.Thumbnail.book_id == book.id).first()
|
||||||
|
# return send_from_directory(thumbnails.get_thumbnail_cache_dir(), thumbnail.filename)
|
||||||
|
# else:
|
||||||
|
# WorkerThread.add(None, TaskThumbnail(book, _(u'Generating cover thumbnail for: ' + book.title)))
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
try:
|
try:
|
||||||
if not gd.is_gdrive_ready():
|
if not gd.is_gdrive_ready():
|
||||||
|
@ -561,8 +566,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||||
else:
|
else:
|
||||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||||
|
|
154
cps/tasks/thumbnail.py
Normal file
154
cps/tasks/thumbnail.py
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2020 mmonkey
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cps import config, db, gdriveutils, logger, ub
|
||||||
|
from cps.constants import CACHE_DIR as _CACHE_DIR
|
||||||
|
from cps.services.worker import CalibreTask
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
try:
|
||||||
|
from wand.image import Image
|
||||||
|
use_IM = True
|
||||||
|
except (ImportError, RuntimeError) as e:
|
||||||
|
use_IM = False
|
||||||
|
|
||||||
|
THUMBNAIL_RESOLUTION_1X = 1.0
|
||||||
|
THUMBNAIL_RESOLUTION_2X = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
class TaskThumbnail(CalibreTask):
|
||||||
|
def __init__(self, limit=100, task_message=u'Generating cover thumbnails'):
|
||||||
|
super(TaskThumbnail, self).__init__(task_message)
|
||||||
|
self.limit = limit
|
||||||
|
self.log = logger.create()
|
||||||
|
self.app_db_session = ub.get_new_session_instance()
|
||||||
|
self.worker_db = db.CalibreDB(expire_on_commit=False)
|
||||||
|
|
||||||
|
def run(self, worker_thread):
|
||||||
|
if self.worker_db.session and use_IM:
|
||||||
|
thumbnails = self.get_thumbnail_book_ids()
|
||||||
|
thumbnail_book_ids = list(map(lambda t: t.book_id, thumbnails))
|
||||||
|
self.log.info(','.join([str(elem) for elem in thumbnail_book_ids]))
|
||||||
|
self.log.info(len(thumbnail_book_ids))
|
||||||
|
|
||||||
|
books_without_thumbnails = self.get_books_without_thumbnails(thumbnail_book_ids)
|
||||||
|
|
||||||
|
count = len(books_without_thumbnails)
|
||||||
|
for i, book in enumerate(books_without_thumbnails):
|
||||||
|
thumbnails = self.get_thumbnails_for_book(thumbnails, book)
|
||||||
|
if thumbnails:
|
||||||
|
for thumbnail in thumbnails:
|
||||||
|
self.update_book_thumbnail(book, thumbnail)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.create_book_thumbnail(book, THUMBNAIL_RESOLUTION_1X)
|
||||||
|
self.create_book_thumbnail(book, THUMBNAIL_RESOLUTION_2X)
|
||||||
|
|
||||||
|
self.progress = (1.0 / count) * i
|
||||||
|
|
||||||
|
self._handleSuccess()
|
||||||
|
self.app_db_session.close()
|
||||||
|
|
||||||
|
def get_thumbnail_book_ids(self):
|
||||||
|
return self.app_db_session\
|
||||||
|
.query(ub.Thumbnail)\
|
||||||
|
.group_by(ub.Thumbnail.book_id)\
|
||||||
|
.having(func.min(ub.Thumbnail.expiration) > datetime.utcnow())\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
def get_books_without_thumbnails(self, thumbnail_book_ids):
|
||||||
|
return self.worker_db.session\
|
||||||
|
.query(db.Books)\
|
||||||
|
.filter(db.Books.has_cover == 1)\
|
||||||
|
.filter(db.Books.id.notin_(thumbnail_book_ids))\
|
||||||
|
.limit(self.limit)\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
def get_thumbnails_for_book(self, thumbnails, book):
|
||||||
|
results = list()
|
||||||
|
for thumbnail in thumbnails:
|
||||||
|
if thumbnail.book_id == book.id:
|
||||||
|
results.append(thumbnail)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_book_thumbnail(self, book, thumbnail):
|
||||||
|
thumbnail.expiration = datetime.utcnow() + timedelta(days=30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.app_db_session.commit()
|
||||||
|
self.generate_book_thumbnail(book, thumbnail)
|
||||||
|
except Exception as ex:
|
||||||
|
self._handleError(u'Error updating book thumbnail: ' + str(ex))
|
||||||
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
|
def create_book_thumbnail(self, book, resolution):
|
||||||
|
thumbnail = ub.Thumbnail()
|
||||||
|
thumbnail.book_id = book.id
|
||||||
|
thumbnail.resolution = resolution
|
||||||
|
|
||||||
|
self.app_db_session.add(thumbnail)
|
||||||
|
try:
|
||||||
|
self.app_db_session.commit()
|
||||||
|
self.generate_book_thumbnail(book, thumbnail)
|
||||||
|
except Exception as ex:
|
||||||
|
self._handleError(u'Error creating book thumbnail: ' + str(ex))
|
||||||
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
|
def generate_book_thumbnail(self, book, thumbnail):
|
||||||
|
if book and thumbnail:
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
self.log.info('google drive thumbnail')
|
||||||
|
else:
|
||||||
|
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg')
|
||||||
|
if os.path.isfile(book_cover_filepath):
|
||||||
|
with Image(filename=book_cover_filepath) as img:
|
||||||
|
height = self.get_thumbnail_height(thumbnail)
|
||||||
|
if img.height > height:
|
||||||
|
width = self.get_thumbnail_width(height, img)
|
||||||
|
img.resize(width=width, height=height, filter='lanczos')
|
||||||
|
img.save(filename=self.get_thumbnail_cache_path(thumbnail))
|
||||||
|
|
||||||
|
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)))
|
||||||
|
|
||||||
|
def get_thumbnail_cache_dir(self):
|
||||||
|
if not os.path.isdir(_CACHE_DIR):
|
||||||
|
os.makedirs(_CACHE_DIR)
|
||||||
|
|
||||||
|
if not os.path.isdir(os.path.join(_CACHE_DIR, 'thumbnails')):
|
||||||
|
os.makedirs(os.path.join(_CACHE_DIR, 'thumbnails'))
|
||||||
|
|
||||||
|
return os.path.join(_CACHE_DIR, 'thumbnails')
|
||||||
|
|
||||||
|
def get_thumbnail_cache_path(self, thumbnail):
|
||||||
|
if thumbnail:
|
||||||
|
return os.path.join(self.get_thumbnail_cache_dir(), thumbnail.filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "Thumbnail"
|
63
cps/thumbnails.py
Normal file
63
cps/thumbnails.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2020 mmonkey
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import logger, ub
|
||||||
|
from .constants import CACHE_DIR as _CACHE_DIR
|
||||||
|
from .services.worker import WorkerThread
|
||||||
|
from .tasks.thumbnail import TaskThumbnail
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
THUMBNAIL_RESOLUTION_1X = 1.0
|
||||||
|
THUMBNAIL_RESOLUTION_2X = 2.0
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def get_thumbnail_cache_dir():
|
||||||
|
if not os.path.isdir(_CACHE_DIR):
|
||||||
|
os.makedirs(_CACHE_DIR)
|
||||||
|
|
||||||
|
if not os.path.isdir(os.path.join(_CACHE_DIR, 'thumbnails')):
|
||||||
|
os.makedirs(os.path.join(_CACHE_DIR, 'thumbnails'))
|
||||||
|
|
||||||
|
return os.path.join(_CACHE_DIR, 'thumbnails')
|
||||||
|
|
||||||
|
|
||||||
|
def get_thumbnail_cache_path(thumbnail):
|
||||||
|
if thumbnail:
|
||||||
|
return os.path.join(get_thumbnail_cache_dir(), thumbnail.filename)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cover_thumbnail_exists_for_book(book):
|
||||||
|
if book and book.has_cover:
|
||||||
|
thumbnail = ub.session.query(ub.Thumbnail).filter(ub.Thumbnail.book_id == book.id).first()
|
||||||
|
if thumbnail and thumbnail.expiration > datetime.utcnow():
|
||||||
|
thumbnail_path = get_thumbnail_cache_path(thumbnail)
|
||||||
|
return thumbnail_path and os.path.isfile(thumbnail_path)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_thumbnails():
|
||||||
|
WorkerThread.add(None, TaskThumbnail())
|
36
cps/ub.py
36
cps/ub.py
|
@ -40,13 +40,14 @@ except ImportError:
|
||||||
oauth_support = False
|
oauth_support = False
|
||||||
from sqlalchemy import create_engine, exc, exists, event
|
from sqlalchemy import create_engine, exc, exists, event
|
||||||
from sqlalchemy import Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey
|
||||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
|
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON, Numeric
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
|
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from . import constants
|
from . import cli, constants
|
||||||
|
|
||||||
|
|
||||||
session = None
|
session = None
|
||||||
|
@ -434,6 +435,28 @@ class RemoteAuthToken(Base):
|
||||||
return '<Token %r>' % self.id
|
return '<Token %r>' % self.id
|
||||||
|
|
||||||
|
|
||||||
|
class Thumbnail(Base):
|
||||||
|
__tablename__ = 'thumbnail'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
book_id = Column(Integer)
|
||||||
|
uuid = Column(String, default=lambda: str(uuid.uuid4()), unique=True)
|
||||||
|
format = Column(String, default='jpeg')
|
||||||
|
resolution = Column(Numeric(precision=2, scale=1, asdecimal=False), default=1.0)
|
||||||
|
expiration = Column(DateTime, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(days=30))
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def extension(self):
|
||||||
|
if self.format == 'jpeg':
|
||||||
|
return 'jpg'
|
||||||
|
else:
|
||||||
|
return self.format
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def filename(self):
|
||||||
|
return self.uuid + '.' + self.extension
|
||||||
|
|
||||||
|
|
||||||
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
# Migrate database to current version, has to be updated after every database change. Currently migration from
|
||||||
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
|
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
|
||||||
# rows with SQL commands
|
# rows with SQL commands
|
||||||
|
@ -451,6 +474,8 @@ def migrate_Database(session):
|
||||||
KoboStatistics.__table__.create(bind=engine)
|
KoboStatistics.__table__.create(bind=engine)
|
||||||
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
||||||
ArchivedBook.__table__.create(bind=engine)
|
ArchivedBook.__table__.create(bind=engine)
|
||||||
|
if not engine.dialect.has_table(engine.connect(), "thumbnail"):
|
||||||
|
Thumbnail.__table__.create(bind=engine)
|
||||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
if not engine.dialect.has_table(engine.connect(), "registration"):
|
||||||
ReadBook.__table__.create(bind=engine)
|
ReadBook.__table__.create(bind=engine)
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
|
@ -676,6 +701,13 @@ def init_db(app_db_path):
|
||||||
create_anonymous_user(session)
|
create_anonymous_user(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_session_instance():
|
||||||
|
new_engine = create_engine(u'sqlite:///{0}'.format(cli.settingspath), echo=False)
|
||||||
|
new_session = scoped_session(sessionmaker())
|
||||||
|
new_session.configure(bind=new_engine)
|
||||||
|
return new_session
|
||||||
|
|
||||||
|
|
||||||
def dispose():
|
def dispose():
|
||||||
global session
|
global session
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user