Display thumbnails on the frontend, generate thumbnails from google drive
This commit is contained in:
parent
21fce9a5b5
commit
e48bdf9d5a
5
cps.py
5
cps.py
|
@ -43,6 +43,7 @@ from cps.gdrive import gdrive
|
|||
from cps.editbooks import editbook
|
||||
from cps.remotelogin import remotelogin
|
||||
from cps.error_handler import init_errorhandler
|
||||
from cps.schedule import register_jobs
|
||||
|
||||
try:
|
||||
from cps.kobo import kobo, get_kobo_activated
|
||||
|
@ -78,6 +79,10 @@ def main():
|
|||
app.register_blueprint(kobo_auth)
|
||||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
|
||||
# Register scheduled jobs
|
||||
register_jobs()
|
||||
|
||||
success = web_server.start()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
|
|
@ -36,8 +36,6 @@ from flask_principal import Principal
|
|||
from . import config_sql, logger, cache_buster, cli, ub, db
|
||||
from .reverseproxy import ReverseProxied
|
||||
from .server import WebServer
|
||||
from .services.background_scheduler import BackgroundScheduler
|
||||
from .tasks.thumbnail import TaskThumbnail
|
||||
|
||||
|
||||
mimetypes.init()
|
||||
|
@ -117,10 +115,6 @@ def create_app():
|
|||
config.config_goodreads_api_secret,
|
||||
config.config_use_goodreads)
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
# Generate 100 book cover thumbnails every 5 minutes
|
||||
scheduler.add_task(user=None, task=lambda: TaskThumbnail(config=config, limit=100), trigger='interval', minutes=5)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ except ImportError:
|
|||
|
||||
from . import calibre_db
|
||||
from .tasks.convert import TaskConvert
|
||||
from . import logger, config, get_locale, db, ub
|
||||
from . import logger, config, get_locale, db, thumbnails, ub
|
||||
from . import gdriveutils as gd
|
||||
from .constants import STATIC_DIR as _STATIC_DIR
|
||||
from .subproc_wrapper import process_wait
|
||||
|
@ -538,24 +538,27 @@ def get_cover_on_failure(use_generic_cover):
|
|||
return None
|
||||
|
||||
|
||||
def get_book_cover(book_id):
|
||||
def get_book_cover(book_id, resolution=1):
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True)
|
||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
|
||||
|
||||
|
||||
def get_book_cover_with_uuid(book_uuid,
|
||||
use_generic_cover_on_failure=True):
|
||||
def get_book_cover_with_uuid(book_uuid, use_generic_cover_on_failure=True):
|
||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||
return get_book_cover_internal(book, use_generic_cover_on_failure)
|
||||
|
||||
|
||||
def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||
def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=1, disable_thumbnail=False):
|
||||
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)))
|
||||
|
||||
# Send the book cover thumbnail if it exists in cache
|
||||
if not disable_thumbnail:
|
||||
thumbnail = get_book_cover_thumbnail(book, resolution)
|
||||
if thumbnail:
|
||||
if os.path.isfile(thumbnails.get_thumbnail_cache_path(thumbnail)):
|
||||
return send_from_directory(thumbnails.get_thumbnail_cache_dir(), thumbnail.filename)
|
||||
|
||||
# Send the book cover from Google Drive if configured
|
||||
if config.config_use_google_drive:
|
||||
try:
|
||||
if not gd.is_gdrive_ready():
|
||||
|
@ -569,6 +572,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
|||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
|
||||
# Send the book cover from the Calibre directory
|
||||
else:
|
||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
|
||||
|
@ -579,6 +584,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
|||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
|
||||
|
||||
def get_book_cover_thumbnail(book, resolution=1):
|
||||
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(ub.Thumbnail.expiration > datetime.utcnow())\
|
||||
.first()
|
||||
|
||||
|
||||
# saves book cover from url
|
||||
def save_cover_from_url(url, book_path):
|
||||
try:
|
||||
|
|
34
cps/schedule.py
Normal file
34
cps/schedule.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# -*- 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
|
||||
|
||||
from . import logger
|
||||
from .services.background_scheduler import BackgroundScheduler
|
||||
from .tasks.thumbnail import TaskThumbnail
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def register_jobs():
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# Generate 100 book cover thumbnails every 5 minutes
|
||||
scheduler.add_task(user=None, task=lambda: TaskThumbnail(limit=100), trigger='interval', minutes=5)
|
||||
|
||||
# TODO: validate thumbnail scheduled task
|
|
@ -19,11 +19,13 @@
|
|||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
|
||||
from cps import db, logger, ub
|
||||
from cps import config, db, gdriveutils, logger, ub
|
||||
from cps.constants import CACHE_DIR as _CACHE_DIR
|
||||
from cps.services.worker import CalibreTask
|
||||
from cps.thumbnails import THUMBNAIL_RESOLUTION_1X, THUMBNAIL_RESOLUTION_2X
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func
|
||||
from urllib.request import urlopen
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
|
@ -31,14 +33,10 @@ try:
|
|||
except (ImportError, RuntimeError) as e:
|
||||
use_IM = False
|
||||
|
||||
THUMBNAIL_RESOLUTION_1X = 1.0
|
||||
THUMBNAIL_RESOLUTION_2X = 2.0
|
||||
|
||||
|
||||
class TaskThumbnail(CalibreTask):
|
||||
def __init__(self, config, limit=100, task_message=u'Generating cover thumbnails'):
|
||||
def __init__(self, limit=100, task_message=u'Generating cover thumbnails'):
|
||||
super(TaskThumbnail, self).__init__(task_message)
|
||||
self.config = config
|
||||
self.limit = limit
|
||||
self.log = logger.create()
|
||||
self.app_db_session = ub.get_new_session_instance()
|
||||
|
@ -114,17 +112,39 @@ class TaskThumbnail(CalibreTask):
|
|||
|
||||
def generate_book_thumbnail(self, book, thumbnail):
|
||||
if book and thumbnail:
|
||||
if self.config.config_use_google_drive:
|
||||
self.log.info('google drive thumbnail')
|
||||
else:
|
||||
book_cover_filepath = os.path.join(self.config.config_calibre_dir, book.path, 'cover.jpg')
|
||||
if os.path.isfile(book_cover_filepath):
|
||||
with Image(filename=book_cover_filepath) as img:
|
||||
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:
|
||||
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))
|
||||
except Exception as ex:
|
||||
# Bubble exception to calling function
|
||||
raise ex
|
||||
finally:
|
||||
stream.close()
|
||||
else:
|
||||
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:
|
||||
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)
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
|
8
cps/templates/book_cover.html
Normal file
8
cps/templates/book_cover.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% macro book_cover_image(book_id, book_title) -%}
|
||||
<img
|
||||
srcset="{{ url_for('web.get_cover', book_id=book_id, resolution=1) }} 1x,
|
||||
{{ url_for('web.get_cover', book_id=book_id, resolution=2) }} 2x"
|
||||
src="{{ url_for('web.get_cover', book_id=book_id) }}"
|
||||
alt="{{ book_title }}"
|
||||
/>
|
||||
{%- endmacro %}
|
|
@ -1,9 +1,11 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
{% if book %}
|
||||
<div class="col-sm-3 col-lg-3 col-xs-12">
|
||||
<div class="cover">
|
||||
<img src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>
|
||||
{{ book_cover_image(book.id, book.title) }}
|
||||
<!-- <img src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>-->
|
||||
</div>
|
||||
{% if g.user.role_delete_books() %}
|
||||
<div class="text-center">
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-3 col-lg-3 col-xs-5">
|
||||
<div class="cover">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
<!-- <img src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9 col-lg-9 book-meta">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="discover load-more">
|
||||
|
@ -8,7 +9,7 @@
|
|||
<div class="cover">
|
||||
{% 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">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
<div class="container-fluid">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
||||
|
@ -28,7 +29,7 @@
|
|||
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
|
||||
<div class="cover">
|
||||
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
||||
{{ book_cover_image(entry[0].id, entry[0].name) }}
|
||||
<span class="badge">{{entry.count}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
{% if g.user.show_detail_random() %}
|
||||
|
@ -8,7 +9,7 @@
|
|||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
@ -82,7 +83,7 @@
|
|||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal %}
|
||||
{% from 'book_cover.html' import book_cover_image %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ g.user.locale }}">
|
||||
<head>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="discover">
|
||||
|
@ -43,7 +44,7 @@
|
|||
<div class="cover">
|
||||
{% 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">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{% from 'book_cover.html' import book_cover_image %}
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="discover">
|
||||
|
@ -30,7 +31,7 @@
|
|||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{{ book_cover_image(entry.id, entry.title) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
|
|
@ -21,13 +21,11 @@ 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
|
||||
THUMBNAIL_RESOLUTION_1X = 1
|
||||
THUMBNAIL_RESOLUTION_2X = 2
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
@ -35,17 +33,14 @@ 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
|
||||
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ except ImportError:
|
|||
oauth_support = False
|
||||
from sqlalchemy import create_engine, exc, exists, event
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON, Numeric
|
||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
@ -442,7 +442,7 @@ class Thumbnail(Base):
|
|||
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)
|
||||
resolution = Column(SmallInteger, default=1)
|
||||
expiration = Column(DateTime, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(days=30))
|
||||
|
||||
@hybrid_property
|
||||
|
|
11
cps/web.py
11
cps/web.py
|
@ -1171,14 +1171,17 @@ def advanced_search_form():
|
|||
|
||||
|
||||
@web.route("/cover/<int:book_id>")
|
||||
@web.route("/cover/<int:book_id>/<int:resolution>")
|
||||
@login_required_if_no_ano
|
||||
def get_cover(book_id):
|
||||
return get_book_cover(book_id)
|
||||
def get_cover(book_id, resolution=1):
|
||||
return get_book_cover(book_id, resolution)
|
||||
|
||||
|
||||
@web.route("/robots.txt")
|
||||
def get_robots():
|
||||
return send_from_directory(constants.STATIC_DIR, "robots.txt")
|
||||
|
||||
|
||||
@web.route("/show/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
|
||||
@web.route("/show/<int:book_id>/<book_format>/<anyname>")
|
||||
@login_required_if_no_ano
|
||||
|
@ -1205,7 +1208,6 @@ def serve_book(book_id, book_format, anyname):
|
|||
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
|
||||
|
||||
|
||||
|
||||
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
|
||||
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
|
||||
@login_required_if_no_ano
|
||||
|
@ -1387,9 +1389,6 @@ def logout():
|
|||
return redirect(url_for('web.login'))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ################################### Users own configuration #########################################################
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user