Added thumbnails
Merge remote-tracking branch 'cover/thumbnails' into development # Conflicts: # cps/admin.py # cps/templates/layout.html # cps/ub.py # cps/web.py Update join for sqlalchemy 1.4
This commit is contained in:
		
							parent
							
								
									5d8d796807
								
							
						
					
					
						commit
						dd30ac4fbd
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -22,6 +22,7 @@ vendor/
 | 
				
			||||||
# calibre-web
 | 
					# calibre-web
 | 
				
			||||||
*.db
 | 
					*.db
 | 
				
			||||||
*.log
 | 
					*.log
 | 
				
			||||||
 | 
					cps/cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.idea/
 | 
					.idea/
 | 
				
			||||||
*.bak
 | 
					*.bak
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								cps.py
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								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.schedule import register_jobs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
    from cps.kobo import kobo, get_kobo_activated
 | 
					    from cps.kobo import kobo, get_kobo_activated
 | 
				
			||||||
| 
						 | 
					@ -78,6 +79,10 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Register scheduled jobs
 | 
				
			||||||
 | 
					    register_jobs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    success = web_server.start()
 | 
					    success = web_server.start()
 | 
				
			||||||
    sys.exit(0 if success else 1)
 | 
					    sys.exit(0 if success else 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,7 @@ def create_app():
 | 
				
			||||||
        app.instance_path = app.instance_path.decode('utf-8')
 | 
					        app.instance_path = app.instance_path.decode('utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if os.environ.get('FLASK_DEBUG'):
 | 
					    if os.environ.get('FLASK_DEBUG'):
 | 
				
			||||||
    	cache_buster.init_cache_busting(app)
 | 
					        cache_buster.init_cache_busting(app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info('Starting Calibre Web...')
 | 
					    log.info('Starting Calibre Web...')
 | 
				
			||||||
    if sys.version_info < (3, 0):
 | 
					    if sys.version_info < (3, 0):
 | 
				
			||||||
| 
						 | 
					@ -121,6 +121,7 @@ def create_app():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return app
 | 
					    return app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@babel.localeselector
 | 
					@babel.localeselector
 | 
				
			||||||
def get_locale():
 | 
					def get_locale():
 | 
				
			||||||
    # if a user is logged in, use the locale from the user settings
 | 
					    # if a user is logged in, use the locale from the user settings
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								cps/admin.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								cps/admin.py
									
									
									
									
									
								
							| 
						 | 
					@ -40,7 +40,7 @@ from sqlalchemy.orm.attributes import flag_modified
 | 
				
			||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
 | 
					from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
 | 
				
			||||||
from sqlalchemy.sql.expression import func, or_
 | 
					from sqlalchemy.sql.expression import func, or_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import constants, logger, helper, services, isoLanguages
 | 
					from . import constants, logger, helper, services, isoLanguages, fs
 | 
				
			||||||
from .cli import filepicker
 | 
					from .cli import filepicker
 | 
				
			||||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
 | 
					from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
 | 
				
			||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
 | 
					from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
 | 
				
			||||||
| 
						 | 
					@ -164,6 +164,23 @@ def shutdown():
 | 
				
			||||||
    return json.dumps(showtext), 400
 | 
					    return json.dumps(showtext), 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admi.route("/clear-cache")
 | 
				
			||||||
 | 
					@login_required
 | 
				
			||||||
 | 
					@admin_required
 | 
				
			||||||
 | 
					def clear_cache():
 | 
				
			||||||
 | 
					    cache_type = request.args.get('cache_type'.strip())
 | 
				
			||||||
 | 
					    showtext = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if cache_type == fs.CACHE_TYPE_THUMBNAILS:
 | 
				
			||||||
 | 
					        log.info('clearing cover thumbnail cache')
 | 
				
			||||||
 | 
					        showtext['text'] = _(u'Cleared cover thumbnail cache')
 | 
				
			||||||
 | 
					        helper.clear_cover_thumbnail_cache()
 | 
				
			||||||
 | 
					        return json.dumps(showtext)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showtext['text'] = _(u'Unknown command')
 | 
				
			||||||
 | 
					    return json.dumps(showtext)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admi.route("/admin/view")
 | 
					@admi.route("/admin/view")
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
@admin_required
 | 
					@admin_required
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,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")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								cps/db.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cps/db.py
									
									
									
									
									
								
							| 
						 | 
					@ -620,15 +620,18 @@ class CalibreDB():
 | 
				
			||||||
            randm = self.session.query(Books) \
 | 
					            randm = self.session.query(Books) \
 | 
				
			||||||
                .filter(self.common_filters(allow_show_archived)) \
 | 
					                .filter(self.common_filters(allow_show_archived)) \
 | 
				
			||||||
                .order_by(func.random()) \
 | 
					                .order_by(func.random()) \
 | 
				
			||||||
                .limit(self.config.config_random_books)
 | 
					                .limit(self.config.config_random_books) \
 | 
				
			||||||
 | 
					                .all()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            randm = false()
 | 
					            randm = false()
 | 
				
			||||||
        off = int(int(pagesize) * (page - 1))
 | 
					        off = int(int(pagesize) * (page - 1))
 | 
				
			||||||
        query = self.session.query(database) \
 | 
					        query = self.session.query(database)
 | 
				
			||||||
            .filter(db_filter) \
 | 
					        if len(join) == 3:
 | 
				
			||||||
 | 
					            query = query.join(join[0], join[1]).join(join[2], isouter=True)
 | 
				
			||||||
 | 
					        elif len(join) == 2:
 | 
				
			||||||
 | 
					            query = query.join(join[0], join[1], isouter=True)
 | 
				
			||||||
 | 
					        query = query.filter(db_filter)\
 | 
				
			||||||
            .filter(self.common_filters(allow_show_archived))
 | 
					            .filter(self.common_filters(allow_show_archived))
 | 
				
			||||||
        if len(join):
 | 
					 | 
				
			||||||
            query = query.join(*join, isouter=True)
 | 
					 | 
				
			||||||
        entries = list()
 | 
					        entries = list()
 | 
				
			||||||
        pagination = list()
 | 
					        pagination = list()
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -614,6 +614,7 @@ def upload_cover(request, book):
 | 
				
			||||||
                abort(403)
 | 
					                abort(403)
 | 
				
			||||||
            ret, message = helper.save_cover(requested_file, book.path)
 | 
					            ret, message = helper.save_cover(requested_file, book.path)
 | 
				
			||||||
            if ret is True:
 | 
					            if ret is True:
 | 
				
			||||||
 | 
					                helper.clear_cover_thumbnail_cache(book.id)
 | 
				
			||||||
                return True
 | 
					                return True
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                flash(message, category="error")
 | 
					                flash(message, category="error")
 | 
				
			||||||
| 
						 | 
					@ -710,6 +711,7 @@ def edit_book(book_id):
 | 
				
			||||||
                        if result is True:
 | 
					                        if result is True:
 | 
				
			||||||
                            book.has_cover = 1
 | 
					                            book.has_cover = 1
 | 
				
			||||||
                            modif_date = True
 | 
					                            modif_date = True
 | 
				
			||||||
 | 
					                            helper.clear_cover_thumbnail_cache(book.id)
 | 
				
			||||||
                        else:
 | 
					                        else:
 | 
				
			||||||
                            flash(error, category="error")
 | 
					                            flash(error, category="error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								cps/fs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								cps/fs.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					# -*- 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 .constants import CACHE_DIR
 | 
				
			||||||
 | 
					from os import listdir, makedirs, remove
 | 
				
			||||||
 | 
					from os.path import isdir, isfile, join
 | 
				
			||||||
 | 
					from shutil import rmtree
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CACHE_TYPE_THUMBNAILS = 'thumbnails'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileSystem:
 | 
				
			||||||
 | 
					    _instance = None
 | 
				
			||||||
 | 
					    _cache_dir = CACHE_DIR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __new__(cls):
 | 
				
			||||||
 | 
					        if cls._instance is None:
 | 
				
			||||||
 | 
					            cls._instance = super(FileSystem, cls).__new__(cls)
 | 
				
			||||||
 | 
					        return cls._instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_cache_dir(self, cache_type=None):
 | 
				
			||||||
 | 
					        if not isdir(self._cache_dir):
 | 
				
			||||||
 | 
					            makedirs(self._cache_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if cache_type and not isdir(join(self._cache_dir, cache_type)):
 | 
				
			||||||
 | 
					            makedirs(join(self._cache_dir, cache_type))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return join(self._cache_dir, cache_type) if cache_type else self._cache_dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_cache_file_path(self, filename, cache_type=None):
 | 
				
			||||||
 | 
					        return join(self.get_cache_dir(cache_type), filename) if filename else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def list_cache_files(self, cache_type=None):
 | 
				
			||||||
 | 
					        path = self.get_cache_dir(cache_type)
 | 
				
			||||||
 | 
					        return [file for file in listdir(path) if isfile(join(path, file))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete_cache_dir(self, cache_type=None):
 | 
				
			||||||
 | 
					        if not cache_type and isdir(self._cache_dir):
 | 
				
			||||||
 | 
					            rmtree(self._cache_dir)
 | 
				
			||||||
 | 
					        if cache_type and isdir(join(self._cache_dir, cache_type)):
 | 
				
			||||||
 | 
					            rmtree(join(self._cache_dir, cache_type))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete_cache_file(self, filename, cache_type=None):
 | 
				
			||||||
 | 
					        if isfile(join(self.get_cache_dir(cache_type), filename)):
 | 
				
			||||||
 | 
					            remove(join(self.get_cache_dir(cache_type), filename))
 | 
				
			||||||
| 
						 | 
					@ -52,12 +52,13 @@ except ImportError:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import calibre_db
 | 
					from . import calibre_db
 | 
				
			||||||
from .tasks.convert import TaskConvert
 | 
					from .tasks.convert import TaskConvert
 | 
				
			||||||
from . import logger, config, get_locale, db, 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
 | 
				
			||||||
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
 | 
				
			||||||
 | 
					from .tasks.thumbnail import TaskClearCoverThumbnailCache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log = logger.create()
 | 
					log = logger.create()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -514,12 +515,32 @@ def update_dir_stucture(book_id, calibrepath, first_author=None, orignal_filepat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def delete_book(book, calibrepath, book_format):
 | 
					def delete_book(book, calibrepath, book_format):
 | 
				
			||||||
 | 
					    clear_cover_thumbnail_cache(book.id)
 | 
				
			||||||
    if config.config_use_google_drive:
 | 
					    if config.config_use_google_drive:
 | 
				
			||||||
        return delete_book_gdrive(book, book_format)
 | 
					        return delete_book_gdrive(book, book_format)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        return delete_book_file(book, calibrepath, book_format)
 | 
					        return delete_book_file(book, calibrepath, book_format)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_thumbnails_for_books(books):
 | 
				
			||||||
 | 
					    books_with_covers = list(filter(lambda b: b.has_cover, books))
 | 
				
			||||||
 | 
					    book_ids = list(map(lambda b: b.id, books_with_covers))
 | 
				
			||||||
 | 
					    cache = fs.FileSystem()
 | 
				
			||||||
 | 
					    thumbnail_files = cache.list_cache_files(fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ub.session\
 | 
				
			||||||
 | 
					        .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					        .filter(ub.Thumbnail.book_id.in_(book_ids))\
 | 
				
			||||||
 | 
					        .filter(ub.Thumbnail.filename.in_(thumbnail_files))\
 | 
				
			||||||
 | 
					        .filter(ub.Thumbnail.expiration > datetime.utcnow())\
 | 
				
			||||||
 | 
					        .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_thumbnails_for_book_series(series):
 | 
				
			||||||
 | 
					    books = list(map(lambda s: s[0], series))
 | 
				
			||||||
 | 
					    return get_thumbnails_for_books(books)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_cover_on_failure(use_generic_cover):
 | 
					def get_cover_on_failure(use_generic_cover):
 | 
				
			||||||
    if use_generic_cover:
 | 
					    if use_generic_cover:
 | 
				
			||||||
        return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
 | 
					        return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
 | 
				
			||||||
| 
						 | 
					@ -532,14 +553,54 @@ def get_book_cover(book_id):
 | 
				
			||||||
    return get_book_cover_internal(book, use_generic_cover_on_failure=True)
 | 
					    return get_book_cover_internal(book, use_generic_cover_on_failure=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_book_cover_with_uuid(book_uuid,
 | 
					def get_book_cover_with_uuid(book_uuid, use_generic_cover_on_failure=True):
 | 
				
			||||||
                             use_generic_cover_on_failure=True):
 | 
					 | 
				
			||||||
    book = calibre_db.get_book_by_uuid(book_uuid)
 | 
					    book = calibre_db.get_book_by_uuid(book_uuid)
 | 
				
			||||||
    return get_book_cover_internal(book, use_generic_cover_on_failure)
 | 
					    return get_book_cover_internal(book, use_generic_cover_on_failure)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_book_cover_internal(book, use_generic_cover_on_failure):
 | 
					def get_cached_book_cover(cache_id):
 | 
				
			||||||
 | 
					    parts = cache_id.split('_')
 | 
				
			||||||
 | 
					    book_uuid = parts[0] if len(parts) else None
 | 
				
			||||||
 | 
					    resolution = parts[2] if len(parts) > 2 else None
 | 
				
			||||||
 | 
					    book = calibre_db.get_book_by_uuid(book_uuid) if book_uuid else None
 | 
				
			||||||
 | 
					    return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_cached_book_cover_thumbnail(cache_id):
 | 
				
			||||||
 | 
					    parts = cache_id.split('_')
 | 
				
			||||||
 | 
					    thumbnail_uuid = parts[0] if len(parts) else None
 | 
				
			||||||
 | 
					    thumbnail = None
 | 
				
			||||||
 | 
					    if thumbnail_uuid:
 | 
				
			||||||
 | 
					        thumbnail = ub.session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					            .filter(ub.Thumbnail.uuid == thumbnail_uuid)\
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if thumbnail and thumbnail.expiration > datetime.utcnow():
 | 
				
			||||||
 | 
					        cache = fs.FileSystem()
 | 
				
			||||||
 | 
					        if cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
 | 
				
			||||||
 | 
					            return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    elif thumbnail:
 | 
				
			||||||
 | 
					        book = calibre_db.get_book(thumbnail.book_id)
 | 
				
			||||||
 | 
					        return get_book_cover_internal(book, use_generic_cover_on_failure=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return get_cover_on_failure(True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None):
 | 
				
			||||||
    if book and book.has_cover:
 | 
					    if book and book.has_cover:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Send the book cover thumbnail if it exists in cache
 | 
				
			||||||
 | 
					        if resolution:
 | 
				
			||||||
 | 
					            thumbnail = get_book_cover_thumbnail(book, resolution)
 | 
				
			||||||
 | 
					            if thumbnail:
 | 
				
			||||||
 | 
					                cache = fs.FileSystem()
 | 
				
			||||||
 | 
					                if cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS):
 | 
				
			||||||
 | 
					                    return send_from_directory(cache.get_cache_dir(fs.CACHE_TYPE_THUMBNAILS), thumbnail.filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Send the book cover from Google Drive if configured
 | 
				
			||||||
        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():
 | 
				
			||||||
| 
						 | 
					@ -550,9 +611,11 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Send the book cover from the Calibre directory
 | 
				
			||||||
        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)
 | 
				
			||||||
            if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
 | 
					            if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
 | 
				
			||||||
| 
						 | 
					@ -563,6 +626,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
 | 
				
			||||||
        return get_cover_on_failure(use_generic_cover_on_failure)
 | 
					        return get_cover_on_failure(use_generic_cover_on_failure)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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(ub.Thumbnail.expiration > datetime.utcnow())\
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# saves book cover from url
 | 
					# saves book cover from url
 | 
				
			||||||
def save_cover_from_url(url, book_path):
 | 
					def save_cover_from_url(url, book_path):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -820,3 +893,7 @@ def get_download_link(book_id, book_format, client):
 | 
				
			||||||
        return do_download_file(book, book_format, client, data1, headers)
 | 
					        return do_download_file(book, book_format, client, data1, headers)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def clear_cover_thumbnail_cache(book_id=None):
 | 
				
			||||||
 | 
					    WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -128,8 +128,30 @@ def formatseriesindex_filter(series_index):
 | 
				
			||||||
            return series_index
 | 
					            return series_index
 | 
				
			||||||
    return 0
 | 
					    return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@jinjia.app_template_filter('uuidfilter')
 | 
					@jinjia.app_template_filter('uuidfilter')
 | 
				
			||||||
def uuidfilter(var):
 | 
					def uuidfilter(var):
 | 
				
			||||||
    return uuid4()
 | 
					    return uuid4()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@jinjia.app_template_filter('book_cover_cache_id')
 | 
				
			||||||
 | 
					def book_cover_cache_id(book, resolution=None):
 | 
				
			||||||
 | 
					    timestamp = int(book.last_modified.timestamp() * 1000)
 | 
				
			||||||
 | 
					    cache_bust = str(book.uuid) + '_' + str(timestamp)
 | 
				
			||||||
 | 
					    return cache_bust if not resolution else cache_bust + '_' + str(resolution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@jinjia.app_template_filter('get_book_thumbnails')
 | 
				
			||||||
 | 
					def get_book_thumbnails(book_id, thumbnails=None):
 | 
				
			||||||
 | 
					    return list(filter(lambda t: t.book_id == book_id, thumbnails)) if book_id > -1 and thumbnails else list()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@jinjia.app_template_filter('get_book_thumbnail_srcset')
 | 
				
			||||||
 | 
					def get_book_thumbnail_srcset(thumbnails):
 | 
				
			||||||
 | 
					    srcset = list()
 | 
				
			||||||
 | 
					    for thumbnail in thumbnails:
 | 
				
			||||||
 | 
					        timestamp = int(thumbnail.generated_at.timestamp() * 1000)
 | 
				
			||||||
 | 
					        cache_id = str(thumbnail.uuid) + '_' + str(timestamp)
 | 
				
			||||||
 | 
					        url = url_for('web.get_cached_cover_thumbnail', cache_id=cache_id)
 | 
				
			||||||
 | 
					        srcset.append(url + ' ' + str(thumbnail.resolution) + 'x')
 | 
				
			||||||
 | 
					    return ', '.join(srcset)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										36
									
								
								cps/schedule.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								cps/schedule.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					# -*- 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 .services.background_scheduler import BackgroundScheduler
 | 
				
			||||||
 | 
					from .tasks.database import TaskReconnectDatabase
 | 
				
			||||||
 | 
					from .tasks.thumbnail import TaskSyncCoverThumbnailCache, TaskGenerateCoverThumbnails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def register_jobs():
 | 
				
			||||||
 | 
					    scheduler = BackgroundScheduler()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Generate 100 book cover thumbnails every 5 minutes
 | 
				
			||||||
 | 
					    scheduler.add_task(user=None, task=lambda: TaskGenerateCoverThumbnails(limit=100), trigger='cron', minute='*/5')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Cleanup book cover cache every 6 hours
 | 
				
			||||||
 | 
					    scheduler.add_task(user=None, task=lambda: TaskSyncCoverThumbnailCache(), trigger='cron', minute='15', hour='*/6')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Reconnect metadata.db every 4 hours
 | 
				
			||||||
 | 
					    scheduler.add_task(user=None, task=lambda: TaskReconnectDatabase(), trigger='cron', minute='5', hour='*/4')
 | 
				
			||||||
							
								
								
									
										52
									
								
								cps/services/background_scheduler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								cps/services/background_scheduler.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					# -*- 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 atexit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .. import logger
 | 
				
			||||||
 | 
					from .worker import WorkerThread
 | 
				
			||||||
 | 
					from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BackgroundScheduler:
 | 
				
			||||||
 | 
					    _instance = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __new__(cls):
 | 
				
			||||||
 | 
					        if cls._instance is None:
 | 
				
			||||||
 | 
					            cls._instance = super(BackgroundScheduler, cls).__new__(cls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            scheduler = BScheduler()
 | 
				
			||||||
 | 
					            atexit.register(lambda: scheduler.shutdown())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cls.log = logger.create()
 | 
				
			||||||
 | 
					            cls.scheduler = scheduler
 | 
				
			||||||
 | 
					            cls.scheduler.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cls._instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add(self, func, trigger, **trigger_args):
 | 
				
			||||||
 | 
					        self.scheduler.add_job(func=func, trigger=trigger, **trigger_args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_task(self, user, task, trigger, **trigger_args):
 | 
				
			||||||
 | 
					        def scheduled_task():
 | 
				
			||||||
 | 
					            worker_task = task()
 | 
				
			||||||
 | 
					            self.log.info('Running scheduled task in background: ' + worker_task.name + ': ' + worker_task.message)
 | 
				
			||||||
 | 
					            WorkerThread.add(user, worker_task)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.add(func=scheduled_task, trigger=trigger, **trigger_args)
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,6 @@ def _get_main_thread():
 | 
				
			||||||
    raise Exception("main thread not found?!")
 | 
					    raise Exception("main thread not found?!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImprovedQueue(queue.Queue):
 | 
					class ImprovedQueue(queue.Queue):
 | 
				
			||||||
    def to_list(self):
 | 
					    def to_list(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -45,7 +44,8 @@ class ImprovedQueue(queue.Queue):
 | 
				
			||||||
        with self.mutex:
 | 
					        with self.mutex:
 | 
				
			||||||
            return list(self.queue)
 | 
					            return list(self.queue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#Class for all worker tasks in the background
 | 
					
 | 
				
			||||||
 | 
					# Class for all worker tasks in the background
 | 
				
			||||||
class WorkerThread(threading.Thread):
 | 
					class WorkerThread(threading.Thread):
 | 
				
			||||||
    _instance = None
 | 
					    _instance = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -127,6 +127,10 @@ class WorkerThread(threading.Thread):
 | 
				
			||||||
                # CalibreTask.start() should wrap all exceptions in it's own error handling
 | 
					                # CalibreTask.start() should wrap all exceptions in it's own error handling
 | 
				
			||||||
                item.task.start(self)
 | 
					                item.task.start(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # remove self_cleanup tasks from list
 | 
				
			||||||
 | 
					            if item.task.self_cleanup:
 | 
				
			||||||
 | 
					                self.dequeued.remove(item)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.queue.task_done()
 | 
					            self.queue.task_done()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -141,6 +145,7 @@ class CalibreTask:
 | 
				
			||||||
        self.end_time = None
 | 
					        self.end_time = None
 | 
				
			||||||
        self.message = message
 | 
					        self.message = message
 | 
				
			||||||
        self.id = uuid.uuid4()
 | 
					        self.id = uuid.uuid4()
 | 
				
			||||||
 | 
					        self.self_cleanup = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @abc.abstractmethod
 | 
					    @abc.abstractmethod
 | 
				
			||||||
    def run(self, worker_thread):
 | 
					    def run(self, worker_thread):
 | 
				
			||||||
| 
						 | 
					@ -209,6 +214,14 @@ class CalibreTask:
 | 
				
			||||||
        # todo: throw error if outside of [0,1]
 | 
					        # todo: throw error if outside of [0,1]
 | 
				
			||||||
        self._progress = x
 | 
					        self._progress = x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def self_cleanup(self):
 | 
				
			||||||
 | 
					        return self._self_cleanup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @self_cleanup.setter
 | 
				
			||||||
 | 
					    def self_cleanup(self, is_self_cleanup):
 | 
				
			||||||
 | 
					        self._self_cleanup = is_self_cleanup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _handleError(self, error_message):
 | 
					    def _handleError(self, error_message):
 | 
				
			||||||
        self.stat = STAT_FAIL
 | 
					        self.stat = STAT_FAIL
 | 
				
			||||||
        self.progress = 1
 | 
					        self.progress = 1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -403,7 +403,7 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
 | 
				
			||||||
                                                            db.Books,
 | 
					                                                            db.Books,
 | 
				
			||||||
                                                            ub.BookShelf.shelf == shelf_id,
 | 
					                                                            ub.BookShelf.shelf == shelf_id,
 | 
				
			||||||
                                                            [ub.BookShelf.order.asc()],
 | 
					                                                            [ub.BookShelf.order.asc()],
 | 
				
			||||||
                                                            ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
 | 
					                                                            ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
 | 
				
			||||||
        # delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
 | 
					        # delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
 | 
				
			||||||
        wrong_entries = calibre_db.session.query(ub.BookShelf)\
 | 
					        wrong_entries = calibre_db.session.query(ub.BookShelf)\
 | 
				
			||||||
            .join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
 | 
					            .join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5148,7 +5148,7 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
 | 
				
			||||||
    pointer-events: none
 | 
					    pointer-events: none
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#DeleteDomain:hover:before, #RestartDialog:hover:before, #ShutdownDialog:hover:before, #StatusDialog:hover:before, #deleteButton, #deleteModal:hover:before, body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover td > a:hover {
 | 
					#DeleteDomain:hover:before, #RestartDialog:hover:before, #ShutdownDialog:hover:before, #StatusDialog:hover:before, #ClearCacheDialog:hover:before, #deleteButton, #deleteModal:hover:before, body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover td > a:hover {
 | 
				
			||||||
    cursor: pointer
 | 
					    cursor: pointer
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5235,7 +5235,7 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
 | 
				
			||||||
    margin-bottom: 20px
 | 
					    margin-bottom: 20px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body.admin:not(.modal-open) .btn-default {
 | 
					body.admin .btn-default {
 | 
				
			||||||
    margin-bottom: 10px
 | 
					    margin-bottom: 10px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5466,7 +5466,7 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    z-index: 0 !important
 | 
					    z-index: 0 !important
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog, #ShutdownDialog, #StatusDialog, #deleteModal {
 | 
					#RestartDialog, #ShutdownDialog, #StatusDialog, #ClearCacheDialog, #deleteModal {
 | 
				
			||||||
    top: 0;
 | 
					    top: 0;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
    padding-top: 70px;
 | 
					    padding-top: 70px;
 | 
				
			||||||
| 
						 | 
					@ -5476,7 +5476,7 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    background: rgba(0, 0, 0, .5)
 | 
					    background: rgba(0, 0, 0, .5)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #deleteModal:before {
 | 
					#RestartDialog:before, #ShutdownDialog:before, #StatusDialog:before, #ClearCacheDialog:before, #deleteModal:before {
 | 
				
			||||||
    content: "\E208";
 | 
					    content: "\E208";
 | 
				
			||||||
    padding-right: 10px;
 | 
					    padding-right: 10px;
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
| 
						 | 
					@ -5498,18 +5498,18 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    z-index: 99
 | 
					    z-index: 99
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #deleteModal.in:before {
 | 
					#RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before {
 | 
				
			||||||
    -webkit-transform: translate(0, 0);
 | 
					    -webkit-transform: translate(0, 0);
 | 
				
			||||||
    -ms-transform: translate(0, 0);
 | 
					    -ms-transform: translate(0, 0);
 | 
				
			||||||
    transform: translate(0, 0)
 | 
					    transform: translate(0, 0)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #deleteModal > .modal-dialog {
 | 
					#RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog {
 | 
				
			||||||
    width: 450px;
 | 
					    width: 450px;
 | 
				
			||||||
    margin: auto
 | 
					    margin: auto
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
 | 
					#RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #ClearCacheDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
 | 
				
			||||||
    max-height: calc(100% - 90px);
 | 
					    max-height: calc(100% - 90px);
 | 
				
			||||||
    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
 | 
					    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
 | 
				
			||||||
    box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
 | 
					    box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
 | 
				
			||||||
| 
						 | 
					@ -5520,7 +5520,7 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    width: 450px
 | 
					    width: 450px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header, #StatusDialog > .modal-dialog > .modal-content > .modal-header, #deleteModal > .modal-dialog > .modal-content > .modal-header {
 | 
					#RestartDialog > .modal-dialog > .modal-content > .modal-header, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header, #StatusDialog > .modal-dialog > .modal-content > .modal-header, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header, #deleteModal > .modal-dialog > .modal-content > .modal-header {
 | 
				
			||||||
    padding: 15px 20px;
 | 
					    padding: 15px 20px;
 | 
				
			||||||
    border-radius: 3px 3px 0 0;
 | 
					    border-radius: 3px 3px 0 0;
 | 
				
			||||||
    line-height: 1.71428571;
 | 
					    line-height: 1.71428571;
 | 
				
			||||||
| 
						 | 
					@ -5533,7 +5533,7 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    text-align: left
 | 
					    text-align: left
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before, #StatusDialog > .modal-dialog > .modal-content > .modal-header:before, #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
 | 
					#RestartDialog > .modal-dialog > .modal-content > .modal-header:before, #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before, #StatusDialog > .modal-dialog > .modal-content > .modal-header:before, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before, #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
 | 
				
			||||||
    padding-right: 10px;
 | 
					    padding-right: 10px;
 | 
				
			||||||
    font-size: 18px;
 | 
					    font-size: 18px;
 | 
				
			||||||
    color: #999;
 | 
					    color: #999;
 | 
				
			||||||
| 
						 | 
					@ -5557,6 +5557,11 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    font-family: plex-icons-new, serif
 | 
					    font-family: plex-icons-new, serif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:before {
 | 
				
			||||||
 | 
					    content: "\EA15";
 | 
				
			||||||
 | 
					    font-family: plex-icons-new, serif
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
 | 
					#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
 | 
				
			||||||
    content: "\EA6D";
 | 
					    content: "\EA6D";
 | 
				
			||||||
    font-family: plex-icons-new, serif
 | 
					    font-family: plex-icons-new, serif
 | 
				
			||||||
| 
						 | 
					@ -5580,6 +5585,12 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    font-size: 20px
 | 
					    font-size: 20px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-header:after {
 | 
				
			||||||
 | 
					    content: "Clear Cover Thumbnail Cache";
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    font-size: 20px
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:after {
 | 
					#deleteModal > .modal-dialog > .modal-content > .modal-header:after {
 | 
				
			||||||
    content: "Delete Book";
 | 
					    content: "Delete Book";
 | 
				
			||||||
    display: inline-block;
 | 
					    display: inline-block;
 | 
				
			||||||
| 
						 | 
					@ -5610,7 +5621,17 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    text-align: left
 | 
					    text-align: left
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-body > p, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > p, #StatusDialog > .modal-dialog > .modal-content > .modal-body > p, #deleteModal > .modal-dialog > .modal-content > .modal-body > p {
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body {
 | 
				
			||||||
 | 
					    padding: 20px 20px 10px;
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					    line-height: 1.6em;
 | 
				
			||||||
 | 
					    font-family: Open Sans Regular, Helvetica Neue, Helvetica, Arial, sans-serif;
 | 
				
			||||||
 | 
					    color: #eee;
 | 
				
			||||||
 | 
					    background: #282828;
 | 
				
			||||||
 | 
					    text-align: left
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#RestartDialog > .modal-dialog > .modal-content > .modal-body > p, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > p, #StatusDialog > .modal-dialog > .modal-content > .modal-body > p, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > p, #deleteModal > .modal-dialog > .modal-content > .modal-body > p {
 | 
				
			||||||
    padding: 20px 20px 0 0;
 | 
					    padding: 20px 20px 0 0;
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
    line-height: 1.6em;
 | 
					    line-height: 1.6em;
 | 
				
			||||||
| 
						 | 
					@ -5619,7 +5640,7 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    background: #282828
 | 
					    background: #282828
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart), #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown), #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
 | 
					#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart), #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown), #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache), #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
 | 
				
			||||||
    float: right;
 | 
					    float: right;
 | 
				
			||||||
    z-index: 9;
 | 
					    z-index: 9;
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
| 
						 | 
					@ -5655,6 +5676,18 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    border-radius: 3px
 | 
					    border-radius: 3px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > #clear_cache {
 | 
				
			||||||
 | 
					    float: right;
 | 
				
			||||||
 | 
					    z-index: 9;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    margin: 25px 0 0 10px;
 | 
				
			||||||
 | 
					    min-width: 80px;
 | 
				
			||||||
 | 
					    padding: 10px 18px;
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					    line-height: 1.33;
 | 
				
			||||||
 | 
					    border-radius: 3px
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
 | 
					#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-danger {
 | 
				
			||||||
    float: right;
 | 
					    float: right;
 | 
				
			||||||
    z-index: 9;
 | 
					    z-index: 9;
 | 
				
			||||||
| 
						 | 
					@ -5675,11 +5708,15 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    margin: 55px 0 0 10px
 | 
					    margin: 55px 0 0 10px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache) {
 | 
				
			||||||
 | 
					    margin: 25px 0 0 10px
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
 | 
					#deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default {
 | 
				
			||||||
    margin: 0 0 0 10px
 | 
					    margin: 0 0 0 10px
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart):hover, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown):hover, #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover {
 | 
					#RestartDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#restart):hover, #ShutdownDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#shutdown):hover, #ClearCacheDialog > .modal-dialog > .modal-content > .modal-body > .btn-default:not(#clear_cache):hover, #deleteModal > .modal-dialog > .modal-content > .modal-footer > .btn-default:hover {
 | 
				
			||||||
    background-color: hsla(0, 0%, 100%, .3)
 | 
					    background-color: hsla(0, 0%, 100%, .3)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5713,6 +5750,21 @@ body.admin.modal-open .navbar {
 | 
				
			||||||
    box-shadow: 0 5px 15px rgba(0, 0, 0, .5)
 | 
					    box-shadow: 0 5px 15px rgba(0, 0, 0, .5)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ClearCacheDialog > .modal-dialog > .modal-content > .modal-body:after {
 | 
				
			||||||
 | 
					    content: '';
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 72px;
 | 
				
			||||||
 | 
					    background-color: #323232;
 | 
				
			||||||
 | 
					    border-radius: 0 0 3px 3px;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    z-index: 0;
 | 
				
			||||||
 | 
					    border-top: 1px solid #222;
 | 
				
			||||||
 | 
					    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);
 | 
				
			||||||
 | 
					    box-shadow: 0 5px 15px rgba(0, 0, 0, .5)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#deleteButton {
 | 
					#deleteButton {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    top: 60px;
 | 
					    top: 60px;
 | 
				
			||||||
| 
						 | 
					@ -7299,11 +7351,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
 | 
				
			||||||
        background-color: transparent !important
 | 
					        background-color: transparent !important
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #deleteModal > .modal-dialog {
 | 
					    #RestartDialog > .modal-dialog, #ShutdownDialog > .modal-dialog, #StatusDialog > .modal-dialog, #ClearCacheDialog > .modal-dialog, #deleteModal > .modal-dialog {
 | 
				
			||||||
        max-width: calc(100vw - 40px)
 | 
					        max-width: calc(100vw - 40px)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
 | 
					    #RestartDialog > .modal-dialog > .modal-content, #ShutdownDialog > .modal-dialog > .modal-content, #StatusDialog > .modal-dialog > .modal-content, #ClearCacheDialog > .modal-dialog > .modal-content, #deleteModal > .modal-dialog > .modal-content {
 | 
				
			||||||
        max-width: calc(100vw - 40px);
 | 
					        max-width: calc(100vw - 40px);
 | 
				
			||||||
        left: 0
 | 
					        left: 0
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -7453,7 +7505,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
 | 
				
			||||||
        padding: 30px 15px
 | 
					        padding: 30px 15px
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #deleteModal.in:before {
 | 
					    #RestartDialog.in:before, #ShutdownDialog.in:before, #StatusDialog.in:before, #ClearCacheDialog.in:before, #deleteModal.in:before {
 | 
				
			||||||
        left: auto;
 | 
					        left: auto;
 | 
				
			||||||
        right: 34px
 | 
					        right: 34px
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -430,6 +430,18 @@ $(function() {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    $("#clear_cache").click(function () {
 | 
				
			||||||
 | 
					        $("#spinner3").show();
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            dataType: "json",
 | 
				
			||||||
 | 
					            url: window.location.pathname + "/../../clear-cache",
 | 
				
			||||||
 | 
					            data: {"cache_type":"thumbnails"},
 | 
				
			||||||
 | 
					            success: function(data) {
 | 
				
			||||||
 | 
					                $("#spinner3").hide();
 | 
				
			||||||
 | 
					                $("#ClearCacheDialog").modal("hide");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Init all data control handlers to default
 | 
					    // Init all data control handlers to default
 | 
				
			||||||
    $("input[data-control]").trigger("change");
 | 
					    $("input[data-control]").trigger("change");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										49
									
								
								cps/tasks/database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								cps/tasks/database.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,49 @@
 | 
				
			||||||
 | 
					# -*- 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 cps import config, logger
 | 
				
			||||||
 | 
					from cps.services.worker import CalibreTask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from urllib.request import urlopen
 | 
				
			||||||
 | 
					except ImportError as e:
 | 
				
			||||||
 | 
					    from urllib2 import urlopen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TaskReconnectDatabase(CalibreTask):
 | 
				
			||||||
 | 
					    def __init__(self, task_message=u'Reconnecting Calibre database'):
 | 
				
			||||||
 | 
					        super(TaskReconnectDatabase, self).__init__(task_message)
 | 
				
			||||||
 | 
					        self.log = logger.create()
 | 
				
			||||||
 | 
					        self.listen_address = config.get_config_ipaddress()
 | 
				
			||||||
 | 
					        self.listen_port = config.config_port
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self, worker_thread):
 | 
				
			||||||
 | 
					        address = self.listen_address if self.listen_address else 'localhost'
 | 
				
			||||||
 | 
					        port = self.listen_port if self.listen_port else 8083
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            urlopen('http://' + address + ':' + str(port) + '/reconnect')
 | 
				
			||||||
 | 
					            self._handleSuccess()
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self._handleError(u'Unable to reconnect Calibre database: ' + str(ex))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self):
 | 
				
			||||||
 | 
					        return "Reconnect Database"
 | 
				
			||||||
							
								
								
									
										366
									
								
								cps/tasks/thumbnail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								cps/tasks/thumbnail.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,366 @@
 | 
				
			||||||
 | 
					# -*- 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, fs, gdriveutils, logger, ub
 | 
				
			||||||
 | 
					from cps.services.worker import CalibreTask
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					from sqlalchemy import func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from urllib.request import urlopen
 | 
				
			||||||
 | 
					except ImportError as e:
 | 
				
			||||||
 | 
					    from urllib2 import urlopen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from wand.image import Image
 | 
				
			||||||
 | 
					    use_IM = True
 | 
				
			||||||
 | 
					except (ImportError, RuntimeError) as e:
 | 
				
			||||||
 | 
					    use_IM = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THUMBNAIL_RESOLUTION_1X = 1
 | 
				
			||||||
 | 
					THUMBNAIL_RESOLUTION_2X = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TaskGenerateCoverThumbnails(CalibreTask):
 | 
				
			||||||
 | 
					    def __init__(self, limit=100, task_message=u'Generating cover thumbnails'):
 | 
				
			||||||
 | 
					        super(TaskGenerateCoverThumbnails, self).__init__(task_message)
 | 
				
			||||||
 | 
					        self.limit = limit
 | 
				
			||||||
 | 
					        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 = [
 | 
				
			||||||
 | 
					            THUMBNAIL_RESOLUTION_1X,
 | 
				
			||||||
 | 
					            THUMBNAIL_RESOLUTION_2X
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self, worker_thread):
 | 
				
			||||||
 | 
					        if self.calibre_db.session and use_IM:
 | 
				
			||||||
 | 
					            expired_thumbnails = self.get_expired_thumbnails()
 | 
				
			||||||
 | 
					            thumbnail_book_ids = self.get_thumbnail_book_ids()
 | 
				
			||||||
 | 
					            books_without_thumbnails = self.get_books_without_thumbnails(thumbnail_book_ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            count = len(books_without_thumbnails)
 | 
				
			||||||
 | 
					            if count == 0:
 | 
				
			||||||
 | 
					                # Do not display this task on the frontend if there are no covers to update
 | 
				
			||||||
 | 
					                self.self_cleanup = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for i, book in enumerate(books_without_thumbnails):
 | 
				
			||||||
 | 
					                for resolution in self.resolutions:
 | 
				
			||||||
 | 
					                    expired_thumbnail = self.get_expired_thumbnail_for_book_and_resolution(
 | 
				
			||||||
 | 
					                        book,
 | 
				
			||||||
 | 
					                        resolution,
 | 
				
			||||||
 | 
					                        expired_thumbnails
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    if expired_thumbnail:
 | 
				
			||||||
 | 
					                        self.update_book_thumbnail(book, expired_thumbnail)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        self.create_book_thumbnail(book, resolution)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.message = u'Generating cover thumbnail {0} of {1}'.format(i + 1, count)
 | 
				
			||||||
 | 
					                self.progress = (1.0 / count) * i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._handleSuccess()
 | 
				
			||||||
 | 
					        self.app_db_session.remove()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_expired_thumbnails(self):
 | 
				
			||||||
 | 
					        return self.app_db_session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					            .filter(ub.Thumbnail.expiration < datetime.utcnow())\
 | 
				
			||||||
 | 
					            .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_thumbnail_book_ids(self):
 | 
				
			||||||
 | 
					        return self.app_db_session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail.book_id)\
 | 
				
			||||||
 | 
					            .group_by(ub.Thumbnail.book_id)\
 | 
				
			||||||
 | 
					            .having(func.min(ub.Thumbnail.expiration) > datetime.utcnow())\
 | 
				
			||||||
 | 
					            .distinct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_books_without_thumbnails(self, thumbnail_book_ids):
 | 
				
			||||||
 | 
					        return self.calibre_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_expired_thumbnail_for_book_and_resolution(self, book, resolution, expired_thumbnails):
 | 
				
			||||||
 | 
					        for thumbnail in expired_thumbnails:
 | 
				
			||||||
 | 
					            if thumbnail.book_id == book.id and thumbnail.resolution == resolution:
 | 
				
			||||||
 | 
					                return thumbnail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update_book_thumbnail(self, book, thumbnail):
 | 
				
			||||||
 | 
					        thumbnail.generated_at = datetime.utcnow()
 | 
				
			||||||
 | 
					        thumbnail.expiration = datetime.utcnow() + timedelta(days=30)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					            self.generate_book_thumbnail(book, 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 create_book_thumbnail(self, book, resolution):
 | 
				
			||||||
 | 
					        thumbnail = ub.Thumbnail()
 | 
				
			||||||
 | 
					        thumbnail.book_id = book.id
 | 
				
			||||||
 | 
					        thumbnail.format = 'jpeg'
 | 
				
			||||||
 | 
					        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.log.info(u'Error creating book thumbnail: ' + str(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:
 | 
				
			||||||
 | 
					                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.format = thumbnail.format
 | 
				
			||||||
 | 
					                            filename = self.cache.get_cache_file_path(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					                            img.save(filename=filename)
 | 
				
			||||||
 | 
					                except Exception as ex:
 | 
				
			||||||
 | 
					                    # Bubble exception to calling function
 | 
				
			||||||
 | 
					                    self.log.info(u'Error generating thumbnail file: ' + str(ex))
 | 
				
			||||||
 | 
					                    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.format = thumbnail.format
 | 
				
			||||||
 | 
					                        filename = self.cache.get_cache_file_path(thumbnail.filename, fs.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 TaskSyncCoverThumbnailCache(CalibreTask):
 | 
				
			||||||
 | 
					    def __init__(self, task_message=u'Syncing cover thumbnail cache'):
 | 
				
			||||||
 | 
					        super(TaskSyncCoverThumbnailCache, 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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self, worker_thread):
 | 
				
			||||||
 | 
					        cached_thumbnail_files = self.cache.list_cache_files(fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Expire thumbnails in the database if the cached file is missing
 | 
				
			||||||
 | 
					        # This case will happen if a user deletes the cache dir or cached files
 | 
				
			||||||
 | 
					        if self.app_db_session:
 | 
				
			||||||
 | 
					            self.expire_missing_thumbnails(cached_thumbnail_files)
 | 
				
			||||||
 | 
					            self.progress = 0.25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Delete thumbnails in the database if the book has been removed
 | 
				
			||||||
 | 
					        # This case will happen if a book is removed in Calibre and the metadata.db file is updated in the filesystem
 | 
				
			||||||
 | 
					        if self.app_db_session and self.calibre_db:
 | 
				
			||||||
 | 
					            book_ids = self.get_book_ids()
 | 
				
			||||||
 | 
					            self.delete_thumbnails_for_missing_books(book_ids)
 | 
				
			||||||
 | 
					            self.progress = 0.50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Expire thumbnails in the database if their corresponding book has been updated since they were generated
 | 
				
			||||||
 | 
					        # This case will happen if the book was updated externally
 | 
				
			||||||
 | 
					        if self.app_db_session and self.cache:
 | 
				
			||||||
 | 
					            books = self.get_books_updated_in_the_last_day()
 | 
				
			||||||
 | 
					            book_ids = list(map(lambda b: b.id, books))
 | 
				
			||||||
 | 
					            thumbnails = self.get_thumbnails_for_updated_books(book_ids)
 | 
				
			||||||
 | 
					            self.expire_thumbnails_for_updated_book(books, thumbnails)
 | 
				
			||||||
 | 
					            self.progress = 0.75
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Delete extraneous cached thumbnail files
 | 
				
			||||||
 | 
					        # This case will happen if a book was deleted and the thumbnail OR the metadata.db file was changed externally
 | 
				
			||||||
 | 
					        if self.app_db_session:
 | 
				
			||||||
 | 
					            db_thumbnail_files = self.get_thumbnail_filenames()
 | 
				
			||||||
 | 
					            self.delete_extraneous_thumbnail_files(cached_thumbnail_files, db_thumbnail_files)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._handleSuccess()
 | 
				
			||||||
 | 
					        self.app_db_session.remove()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expire_missing_thumbnails(self, filenames):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session\
 | 
				
			||||||
 | 
					                .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					                .filter(ub.Thumbnail.filename.notin_(filenames))\
 | 
				
			||||||
 | 
					                .update({"expiration": datetime.utcnow()}, synchronize_session=False)
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self.log.info(u'Error expiring thumbnails for missing cache files: ' + str(ex))
 | 
				
			||||||
 | 
					            self._handleError(u'Error expiring thumbnails for missing cache files: ' + str(ex))
 | 
				
			||||||
 | 
					            self.app_db_session.rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_book_ids(self):
 | 
				
			||||||
 | 
					        results = self.calibre_db.session\
 | 
				
			||||||
 | 
					            .query(db.Books.id)\
 | 
				
			||||||
 | 
					            .filter(db.Books.has_cover == 1)\
 | 
				
			||||||
 | 
					            .distinct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [value for value, in results]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete_thumbnails_for_missing_books(self, book_ids):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session\
 | 
				
			||||||
 | 
					                .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					                .filter(ub.Thumbnail.book_id.notin_(book_ids))\
 | 
				
			||||||
 | 
					                .delete(synchronize_session=False)
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self.log.info(str(ex))
 | 
				
			||||||
 | 
					            self._handleError(u'Error deleting thumbnails for missing books: ' + str(ex))
 | 
				
			||||||
 | 
					            self.app_db_session.rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_thumbnail_filenames(self):
 | 
				
			||||||
 | 
					        results = self.app_db_session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail.filename)\
 | 
				
			||||||
 | 
					            .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [thumbnail for thumbnail, in results]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete_extraneous_thumbnail_files(self, cached_thumbnail_files, db_thumbnail_files):
 | 
				
			||||||
 | 
					        extraneous_files = list(set(cached_thumbnail_files).difference(db_thumbnail_files))
 | 
				
			||||||
 | 
					        for file in extraneous_files:
 | 
				
			||||||
 | 
					            self.cache.delete_cache_file(file, fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_books_updated_in_the_last_day(self):
 | 
				
			||||||
 | 
					        return self.calibre_db.session\
 | 
				
			||||||
 | 
					            .query(db.Books)\
 | 
				
			||||||
 | 
					            .filter(db.Books.has_cover == 1)\
 | 
				
			||||||
 | 
					            .filter(db.Books.last_modified > datetime.utcnow() - timedelta(days=1, hours=1))\
 | 
				
			||||||
 | 
					            .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_thumbnails_for_updated_books(self, book_ids):
 | 
				
			||||||
 | 
					        return self.app_db_session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					            .filter(ub.Thumbnail.book_id.in_(book_ids))\
 | 
				
			||||||
 | 
					            .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expire_thumbnails_for_updated_book(self, books, thumbnails):
 | 
				
			||||||
 | 
					        thumbnail_ids = list()
 | 
				
			||||||
 | 
					        for book in books:
 | 
				
			||||||
 | 
					            for thumbnail in thumbnails:
 | 
				
			||||||
 | 
					                if thumbnail.book_id == book.id and thumbnail.generated_at < book.last_modified:
 | 
				
			||||||
 | 
					                    thumbnail_ids.append(thumbnail.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session\
 | 
				
			||||||
 | 
					                .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					                .filter(ub.Thumbnail.id.in_(thumbnail_ids)) \
 | 
				
			||||||
 | 
					                .update({"expiration": datetime.utcnow()}, synchronize_session=False)
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self.log.info(u'Error expiring thumbnails for updated books: ' + str(ex))
 | 
				
			||||||
 | 
					            self._handleError(u'Error expiring thumbnails for updated books: ' + str(ex))
 | 
				
			||||||
 | 
					            self.app_db_session.rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self):
 | 
				
			||||||
 | 
					        return "ThumbnailsSync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TaskClearCoverThumbnailCache(CalibreTask):
 | 
				
			||||||
 | 
					    def __init__(self, book_id=None, task_message=u'Clearing cover thumbnail cache'):
 | 
				
			||||||
 | 
					        super(TaskClearCoverThumbnailCache, self).__init__(task_message)
 | 
				
			||||||
 | 
					        self.log = logger.create()
 | 
				
			||||||
 | 
					        self.book_id = book_id
 | 
				
			||||||
 | 
					        self.app_db_session = ub.get_new_session_instance()
 | 
				
			||||||
 | 
					        self.cache = fs.FileSystem()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self, worker_thread):
 | 
				
			||||||
 | 
					        if self.app_db_session:
 | 
				
			||||||
 | 
					            if self.book_id:
 | 
				
			||||||
 | 
					                thumbnails = self.get_thumbnails_for_book(self.book_id)
 | 
				
			||||||
 | 
					                for thumbnail in thumbnails:
 | 
				
			||||||
 | 
					                    self.expire_and_delete_thumbnail(thumbnail)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.expire_and_delete_all_thumbnails()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._handleSuccess()
 | 
				
			||||||
 | 
					        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)\
 | 
				
			||||||
 | 
					            .all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expire_and_delete_thumbnail(self, thumbnail):
 | 
				
			||||||
 | 
					        thumbnail.expiration = datetime.utcnow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					            self.cache.delete_cache_file(thumbnail.filename, fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self.log.info(u'Error expiring book thumbnail: ' + str(ex))
 | 
				
			||||||
 | 
					            self._handleError(u'Error expiring book thumbnail: ' + str(ex))
 | 
				
			||||||
 | 
					            self.app_db_session.rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expire_and_delete_all_thumbnails(self):
 | 
				
			||||||
 | 
					        self.app_db_session\
 | 
				
			||||||
 | 
					            .query(ub.Thumbnail)\
 | 
				
			||||||
 | 
					            .update({'expiration': datetime.utcnow()})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.app_db_session.commit()
 | 
				
			||||||
 | 
					            self.cache.delete_cache_dir(fs.CACHE_TYPE_THUMBNAILS)
 | 
				
			||||||
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            self.log.info(u'Error expiring book thumbnails: ' + str(ex))
 | 
				
			||||||
 | 
					            self._handleError(u'Error expiring book thumbnails: ' + str(ex))
 | 
				
			||||||
 | 
					            self.app_db_session.rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self):
 | 
				
			||||||
 | 
					        return "ThumbnailsClear"
 | 
				
			||||||
| 
						 | 
					@ -142,15 +142,18 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="row form-group">
 | 
					  <div class="row form-group">
 | 
				
			||||||
    <h2>{{_('Administration')}}</h2>
 | 
					    <h2>{{_('Administration')}}</h2>
 | 
				
			||||||
      <div class="btn btn-default"><a id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a></div>
 | 
					    <div class="btn btn-default"><a id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a></div>
 | 
				
			||||||
      <div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div>
 | 
					    <div class="btn btn-default"><a id="logfile" href="{{url_for('admin.view_logfile')}}">{{_('View Logs')}}</a></div>
 | 
				
			||||||
    </div>
 | 
					  </div>
 | 
				
			||||||
    <div class="row form-group">
 | 
					  <div class="row form-group">
 | 
				
			||||||
      <div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div>
 | 
					    <div class="btn btn-default" id="restart_database" data-toggle="modal" data-target="#StatusDialog">{{_('Reconnect Calibre Database')}}</div>
 | 
				
			||||||
      <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
 | 
					    <div class="btn btn-default" id="clear_cover_thumbnail_cache" data-toggle="modal" data-target="#ClearCacheDialog">{{_('Clear Cover Thumbnail Cache')}}</div>
 | 
				
			||||||
      <div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div class="row form-group">
 | 
				
			||||||
 | 
					    <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart')}}</div>
 | 
				
			||||||
 | 
					    <div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Shutdown')}}</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="row">
 | 
					  <div class="row">
 | 
				
			||||||
| 
						 | 
					@ -231,4 +234,21 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					<div id="ClearCacheDialog" class="modal fade" role="dialog">
 | 
				
			||||||
 | 
					  <div class="modal-dialog modal-sm">
 | 
				
			||||||
 | 
					    <!-- Modal content-->
 | 
				
			||||||
 | 
					    <div class="modal-content">
 | 
				
			||||||
 | 
					      <div class="modal-header bg-info"></div>
 | 
				
			||||||
 | 
					      <div class="modal-body text-center">
 | 
				
			||||||
 | 
					        <p>{{_('Are you sure you want to clear the cover thumbnail cache?')}}</p>
 | 
				
			||||||
 | 
					        <div id="spinner3" class="spinner" style="display:none;">
 | 
				
			||||||
 | 
					          <img id="img-spinner3" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <p></p>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-default" id="clear_cache" >{{_('OK')}}</button>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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">
 | 
				
			||||||
              <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
 | 
					              {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
              {% 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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								cps/templates/book_cover.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								cps/templates/book_cover.html
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					{% macro book_cover_image(book, thumbnails) -%}
 | 
				
			||||||
 | 
					    {%- set book_title = book.title if book.title else book.name -%}
 | 
				
			||||||
 | 
					    {% set srcset = thumbnails|get_book_thumbnail_srcset if thumbnails|length else '' %}
 | 
				
			||||||
 | 
					    {%- if srcset|length -%}
 | 
				
			||||||
 | 
					        <img
 | 
				
			||||||
 | 
					            srcset="{{ srcset }}"
 | 
				
			||||||
 | 
					            src="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id) }}"
 | 
				
			||||||
 | 
					            alt="{{ book_title }}"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    {%- else -%}
 | 
				
			||||||
 | 
					        <img src="{{ url_for('web.get_cached_cover', cache_id=book|book_cover_cache_id) }}" alt="{{ book_title }}" />
 | 
				
			||||||
 | 
					    {%- endif -%}
 | 
				
			||||||
 | 
					{%- endmacro %}
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,10 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_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">
 | 
				
			||||||
        <img src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter)  }}" alt="{{ book.title }}"/>
 | 
					        {{ book_cover_image(book, book.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
    </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">
 | 
				
			||||||
          <img src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />
 | 
					          {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
      </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,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
{% extends "layout.html" %}
 | 
					{% extends "layout.html" %}
 | 
				
			||||||
{% block body %}
 | 
					{% block body %}
 | 
				
			||||||
<div class="discover load-more">
 | 
					<div class="discover load-more">
 | 
				
			||||||
| 
						 | 
					@ -9,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">
 | 
				
			||||||
              <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
 | 
					              {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
              {% 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,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
<div class="container-fluid">
 | 
					<div class="container-fluid">
 | 
				
			||||||
  {% block body %}{% endblock %}
 | 
					  {% block body %}{% endblock %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
{% extends "layout.html" %}
 | 
					{% extends "layout.html" %}
 | 
				
			||||||
{% block body %}
 | 
					{% block body %}
 | 
				
			||||||
<h1 class="{{page}}">{{_(title)}}</h1>
 | 
					<h1 class="{{page}}">{{_(title)}}</h1>
 | 
				
			||||||
| 
						 | 
					@ -29,7 +30,7 @@
 | 
				
			||||||
                  <div class="cover">
 | 
					                  <div class="cover">
 | 
				
			||||||
                      <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
 | 
					                      <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
 | 
				
			||||||
                          <span class="img">
 | 
					                          <span class="img">
 | 
				
			||||||
                              <img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
 | 
					                              {{ book_cover_image(entry[0], entry[0].id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
                              <span class="badge">{{entry.count}}</span>
 | 
					                              <span class="badge">{{entry.count}}</span>
 | 
				
			||||||
                            </span>
 | 
					                            </span>
 | 
				
			||||||
                      </a>
 | 
					                      </a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
{% extends "layout.html" %}
 | 
					{% extends "layout.html" %}
 | 
				
			||||||
{% block body %}
 | 
					{% block body %}
 | 
				
			||||||
{% if g.user.show_detail_random() %}
 | 
					{% if g.user.show_detail_random() %}
 | 
				
			||||||
| 
						 | 
					@ -9,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">
 | 
				
			||||||
                <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
 | 
					                {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
                {% 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>
 | 
				
			||||||
| 
						 | 
					@ -86,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">
 | 
				
			||||||
              <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
 | 
					              {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
              {% 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,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 %}
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="{{ g.user.locale }}">
 | 
					<html lang="{{ g.user.locale }}">
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
{% extends "layout.html" %}
 | 
					{% extends "layout.html" %}
 | 
				
			||||||
{% block body %}
 | 
					{% block body %}
 | 
				
			||||||
<div class="discover">
 | 
					<div class="discover">
 | 
				
			||||||
| 
						 | 
					@ -44,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">
 | 
				
			||||||
                <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
 | 
					                {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
                {% 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,3 +1,4 @@
 | 
				
			||||||
 | 
					{% from 'book_cover.html' import book_cover_image %}
 | 
				
			||||||
{% extends "layout.html" %}
 | 
					{% extends "layout.html" %}
 | 
				
			||||||
{% block body %}
 | 
					{% block body %}
 | 
				
			||||||
<div class="discover">
 | 
					<div class="discover">
 | 
				
			||||||
| 
						 | 
					@ -31,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">
 | 
				
			||||||
                <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
 | 
					                {{ book_cover_image(entry, entry.id|get_book_thumbnails(thumbnails)) }}
 | 
				
			||||||
                {% 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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										34
									
								
								cps/ub.py
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								cps/ub.py
									
									
									
									
									
								
							| 
						 | 
					@ -18,6 +18,7 @@
 | 
				
			||||||
#  along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
					#  along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from __future__ import division, print_function, unicode_literals
 | 
					from __future__ import division, print_function, unicode_literals
 | 
				
			||||||
 | 
					import atexit
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
| 
						 | 
					@ -441,6 +442,27 @@ class RemoteAuthToken(Base):
 | 
				
			||||||
        return '<Token %r>' % self.id
 | 
					        return '<Token %r>' % self.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def filename(context):
 | 
				
			||||||
 | 
					    file_format = context.get_current_parameters()['format']
 | 
				
			||||||
 | 
					    if file_format == 'jpeg':
 | 
				
			||||||
 | 
					        return context.get_current_parameters()['uuid'] + '.jpg'
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return context.get_current_parameters()['uuid'] + '.' + file_format
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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(SmallInteger, default=1)
 | 
				
			||||||
 | 
					    filename = Column(String, default=filename)
 | 
				
			||||||
 | 
					    generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
 | 
				
			||||||
 | 
					    expiration = Column(DateTime, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(days=30))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add missing tables during migration of database
 | 
					# Add missing tables during migration of database
 | 
				
			||||||
def add_missing_tables(engine, session):
 | 
					def add_missing_tables(engine, session):
 | 
				
			||||||
    if not engine.dialect.has_table(engine.connect(), "book_read_link"):
 | 
					    if not engine.dialect.has_table(engine.connect(), "book_read_link"):
 | 
				
			||||||
| 
						 | 
					@ -455,6 +477,8 @@ def add_missing_tables(engine, 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"):
 | 
				
			||||||
        Registration.__table__.create(bind=engine)
 | 
					        Registration.__table__.create(bind=engine)
 | 
				
			||||||
        with engine.connect() as conn:
 | 
					        with engine.connect() as conn:
 | 
				
			||||||
| 
						 | 
					@ -725,6 +749,16 @@ def init_db(app_db_path):
 | 
				
			||||||
            sys.exit(3)
 | 
					            sys.exit(3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    atexit.register(lambda: new_session.remove() if new_session else True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new_session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def dispose():
 | 
					def dispose():
 | 
				
			||||||
    global session
 | 
					    global session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										96
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								cps/web.py
									
									
									
									
									
								
							| 
						 | 
					@ -50,9 +50,10 @@ 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, \
 | 
					from .helper import check_valid_domain, render_task_status, get_cc_columns, get_book_cover, get_cached_book_cover, \
 | 
				
			||||||
    get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
 | 
					    get_cached_book_cover_thumbnail, get_thumbnails_for_books, get_thumbnails_for_book_series, get_download_link, \
 | 
				
			||||||
    send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
 | 
					    send_mail, generate_random_password, send_registration_mail, check_send_to_kindle, check_read_formats, \
 | 
				
			||||||
 | 
					    tags_filters, reset_password
 | 
				
			||||||
from .pagination import Pagination
 | 
					from .pagination import Pagination
 | 
				
			||||||
from .redirect import redirect_back
 | 
					from .redirect import redirect_back
 | 
				
			||||||
from .usermanagement import login_required_if_no_ano
 | 
					from .usermanagement import login_required_if_no_ano
 | 
				
			||||||
| 
						 | 
					@ -411,8 +412,9 @@ def render_books_list(data, sort, book_id, page):
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        website = data or "newest"
 | 
					        website = data or "newest"
 | 
				
			||||||
        entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
 | 
					        entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
					        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
				
			||||||
                                     title=_(u"Books"), page=website)
 | 
					                                     title=_(u"Books"), page=website, thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_rated_books(page, book_id, order):
 | 
					def render_rated_books(page, book_id, order):
 | 
				
			||||||
| 
						 | 
					@ -457,8 +459,9 @@ def render_hot_books(page):
 | 
				
			||||||
                ub.delete_download(book.Downloads.book_id)
 | 
					                ub.delete_download(book.Downloads.book_id)
 | 
				
			||||||
        numBooks = entries.__len__()
 | 
					        numBooks = entries.__len__()
 | 
				
			||||||
        pagination = Pagination(page, config.config_books_per_page, numBooks)
 | 
					        pagination = Pagination(page, config.config_books_per_page, numBooks)
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
					        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
				
			||||||
                                     title=_(u"Hot Books (Most Downloaded)"), page="hot")
 | 
					                                     title=_(u"Hot Books (Most Downloaded)"), page="hot", thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -482,12 +485,14 @@ def render_downloaded_books(page, order):
 | 
				
			||||||
                             .filter(db.Books.id == book.id).first():
 | 
					                             .filter(db.Books.id == book.id).first():
 | 
				
			||||||
                ub.delete_download(book.id)
 | 
					                ub.delete_download(book.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html',
 | 
					        return render_title_template('index.html',
 | 
				
			||||||
                                     random=random,
 | 
					                                     random=random,
 | 
				
			||||||
                                     entries=entries,
 | 
					                                     entries=entries,
 | 
				
			||||||
                                     pagination=pagination,
 | 
					                                     pagination=pagination,
 | 
				
			||||||
                                     title=_(u"Downloaded books by %(user)s",user=current_user.nickname),
 | 
					                                     title=_(u"Downloaded books by %(user)s",user=current_user.nickname),
 | 
				
			||||||
                                     page="download")
 | 
					                                     page="download",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -498,6 +503,7 @@ def render_author_books(page, author_id, order):
 | 
				
			||||||
                                                        db.Books.authors.any(db.Authors.id == author_id),
 | 
					                                                        db.Books.authors.any(db.Authors.id == author_id),
 | 
				
			||||||
                                                        [order[0], db.Series.name, db.Books.series_index],
 | 
					                                                        [order[0], db.Series.name, db.Books.series_index],
 | 
				
			||||||
                                                        db.books_series_link,
 | 
					                                                        db.books_series_link,
 | 
				
			||||||
 | 
					                                                        db.Books.id==db.books_series_link.c.book,
 | 
				
			||||||
                                                        db.Series)
 | 
					                                                        db.Series)
 | 
				
			||||||
    if entries is None or not len(entries):
 | 
					    if entries is None or not len(entries):
 | 
				
			||||||
        flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
 | 
					        flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
 | 
				
			||||||
| 
						 | 
					@ -513,9 +519,10 @@ def render_author_books(page, author_id, order):
 | 
				
			||||||
        author_info = services.goodreads_support.get_author_info(author_name)
 | 
					        author_info = services.goodreads_support.get_author_info(author_name)
 | 
				
			||||||
        other_books = services.goodreads_support.get_other_books(author_info, entries)
 | 
					        other_books = services.goodreads_support.get_other_books(author_info, entries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    thumbnails = get_thumbnails_for_books(entries)
 | 
				
			||||||
    return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id,
 | 
					    return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id,
 | 
				
			||||||
                                 title=_(u"Author: %(name)s", name=author_name), author=author_info,
 | 
					                                 title=_(u"Author: %(name)s", name=author_name), author=author_info,
 | 
				
			||||||
                                 other_books=other_books, page="author")
 | 
					                                 other_books=other_books, page="author", thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_publisher_books(page, book_id, order):
 | 
					def render_publisher_books(page, book_id, order):
 | 
				
			||||||
| 
						 | 
					@ -526,9 +533,12 @@ def render_publisher_books(page, book_id, order):
 | 
				
			||||||
                                                                db.Books.publishers.any(db.Publishers.id == book_id),
 | 
					                                                                db.Books.publishers.any(db.Publishers.id == book_id),
 | 
				
			||||||
                                                                [db.Series.name, order[0], db.Books.series_index],
 | 
					                                                                [db.Series.name, order[0], db.Books.series_index],
 | 
				
			||||||
                                                                db.books_series_link,
 | 
					                                                                db.books_series_link,
 | 
				
			||||||
 | 
					                                                                db.Books.id == db.books_series_link.c.book,
 | 
				
			||||||
                                                                db.Series)
 | 
					                                                                db.Series)
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
 | 
					        return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
 | 
				
			||||||
                                     title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher")
 | 
					                                     title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -540,8 +550,10 @@ def render_series_books(page, book_id, order):
 | 
				
			||||||
                                                                db.Books,
 | 
					                                                                db.Books,
 | 
				
			||||||
                                                                db.Books.series.any(db.Series.id == book_id),
 | 
					                                                                db.Books.series.any(db.Series.id == book_id),
 | 
				
			||||||
                                                                [order[0]])
 | 
					                                                                [order[0]])
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
					        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
				
			||||||
                                     title=_(u"Series: %(serie)s", serie=name.name), page="series")
 | 
					                                     title=_(u"Series: %(serie)s", serie=name.name), page="series",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -553,8 +565,10 @@ def render_ratings_books(page, book_id, order):
 | 
				
			||||||
                                                            db.Books.ratings.any(db.Ratings.id == book_id),
 | 
					                                                            db.Books.ratings.any(db.Ratings.id == book_id),
 | 
				
			||||||
                                                            [order[0]])
 | 
					                                                            [order[0]])
 | 
				
			||||||
    if name and name.rating <= 10:
 | 
					    if name and name.rating <= 10:
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
					        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
				
			||||||
                                     title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), page="ratings")
 | 
					                                     title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)), page="ratings",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -566,8 +580,10 @@ def render_formats_books(page, book_id, order):
 | 
				
			||||||
                                                                db.Books,
 | 
					                                                                db.Books,
 | 
				
			||||||
                                                                db.Books.data.any(db.Data.format == book_id.upper()),
 | 
					                                                                db.Books.data.any(db.Data.format == book_id.upper()),
 | 
				
			||||||
                                                                [order[0]])
 | 
					                                                                [order[0]])
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
					        return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
 | 
				
			||||||
                                     title=_(u"File format: %(format)s", format=name.format), page="formats")
 | 
					                                     title=_(u"File format: %(format)s", format=name.format), page="formats",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -579,9 +595,13 @@ def render_category_books(page, book_id, order):
 | 
				
			||||||
                                                                db.Books,
 | 
					                                                                db.Books,
 | 
				
			||||||
                                                                db.Books.tags.any(db.Tags.id == book_id),
 | 
					                                                                db.Books.tags.any(db.Tags.id == book_id),
 | 
				
			||||||
                                                                [order[0], db.Series.name, db.Books.series_index],
 | 
					                                                                [order[0], db.Series.name, db.Books.series_index],
 | 
				
			||||||
                                                                db.books_series_link, db.Series)
 | 
					                                                                db.books_series_link,
 | 
				
			||||||
 | 
					                                                                db.Books.id == db.books_series_link.c.book,
 | 
				
			||||||
 | 
					                                                                db.Series)
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
 | 
					        return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
 | 
				
			||||||
                                     title=_(u"Category: %(name)s", name=name.name), page="category")
 | 
					                                     title=_(u"Category: %(name)s", name=name.name), page="category",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -599,8 +619,9 @@ def render_language_books(page, name, order):
 | 
				
			||||||
                                                            db.Books,
 | 
					                                                            db.Books,
 | 
				
			||||||
                                                            db.Books.languages.any(db.Languages.lang_code == name),
 | 
					                                                            db.Books.languages.any(db.Languages.lang_code == name),
 | 
				
			||||||
                                                            [order[0]])
 | 
					                                                            [order[0]])
 | 
				
			||||||
 | 
					    thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
    return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
 | 
					    return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
 | 
				
			||||||
                                 title=_(u"Language: %(name)s", name=lang_name), page="language")
 | 
					                                 title=_(u"Language: %(name)s", name=lang_name), page="language", thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_read_books(page, are_read, as_xml=False, order=None):
 | 
					def render_read_books(page, are_read, as_xml=False, order=None):
 | 
				
			||||||
| 
						 | 
					@ -644,8 +665,10 @@ def render_read_books(page, are_read, as_xml=False, order=None):
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
 | 
					            name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
 | 
				
			||||||
            pagename = "unread"
 | 
					            pagename = "unread"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
					        return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
				
			||||||
                                     title=name, page=pagename)
 | 
					                                     title=name, page=pagename, thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_archived_books(page, order):
 | 
					def render_archived_books(page, order):
 | 
				
			||||||
| 
						 | 
					@ -668,8 +691,9 @@ def render_archived_books(page, order):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
 | 
					    name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
 | 
				
			||||||
    pagename = "archived"
 | 
					    pagename = "archived"
 | 
				
			||||||
 | 
					    thumbnails = get_thumbnails_for_books(entries + random)
 | 
				
			||||||
    return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
					    return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
 | 
				
			||||||
                                 title=name, page=pagename)
 | 
					                                 title=name, page=pagename, thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_prepare_search_form(cc):
 | 
					def render_prepare_search_form(cc):
 | 
				
			||||||
| 
						 | 
					@ -702,6 +726,7 @@ def render_prepare_search_form(cc):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_search_results(term, offset=None, order=None, limit=None):
 | 
					def render_search_results(term, offset=None, order=None, limit=None):
 | 
				
			||||||
    entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit)
 | 
					    entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit)
 | 
				
			||||||
 | 
					    thumbnails = get_thumbnails_for_books(entries)
 | 
				
			||||||
    return render_title_template('search.html',
 | 
					    return render_title_template('search.html',
 | 
				
			||||||
                                 searchterm=term,
 | 
					                                 searchterm=term,
 | 
				
			||||||
                                 pagination=pagination,
 | 
					                                 pagination=pagination,
 | 
				
			||||||
| 
						 | 
					@ -710,7 +735,8 @@ def render_search_results(term, offset=None, order=None, limit=None):
 | 
				
			||||||
                                 entries=entries,
 | 
					                                 entries=entries,
 | 
				
			||||||
                                 result_count=result_count,
 | 
					                                 result_count=result_count,
 | 
				
			||||||
                                 title=_(u"Search"),
 | 
					                                 title=_(u"Search"),
 | 
				
			||||||
                                 page="search")
 | 
					                                 page="search",
 | 
				
			||||||
 | 
					                                 thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ################################### View Books list ##################################################################
 | 
					# ################################### View Books list ##################################################################
 | 
				
			||||||
| 
						 | 
					@ -740,6 +766,7 @@ def books_table():
 | 
				
			||||||
    return render_title_template('book_table.html', title=_(u"Books List"), page="book_table",
 | 
					    return render_title_template('book_table.html', title=_(u"Books List"), page="book_table",
 | 
				
			||||||
                                 visiblility=visibility)
 | 
					                                 visiblility=visibility)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@web.route("/ajax/listbooks")
 | 
					@web.route("/ajax/listbooks")
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def list_books():
 | 
					def list_books():
 | 
				
			||||||
| 
						 | 
					@ -772,6 +799,7 @@ def list_books():
 | 
				
			||||||
    response.headers["Content-Type"] = "application/json; charset=utf-8"
 | 
					    response.headers["Content-Type"] = "application/json; charset=utf-8"
 | 
				
			||||||
    return response
 | 
					    return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@web.route("/ajax/table_settings", methods=['POST'])
 | 
					@web.route("/ajax/table_settings", methods=['POST'])
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def update_table_settings():
 | 
					def update_table_settings():
 | 
				
			||||||
| 
						 | 
					@ -826,6 +854,7 @@ def publisher_list():
 | 
				
			||||||
        charlist = calibre_db.session.query(func.upper(func.substr(db.Publishers.name, 1, 1)).label('char')) \
 | 
					        charlist = calibre_db.session.query(func.upper(func.substr(db.Publishers.name, 1, 1)).label('char')) \
 | 
				
			||||||
            .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
 | 
					            .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
 | 
				
			||||||
            .group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
 | 
					            .group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
 | 
					        return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
 | 
				
			||||||
                                     title=_(u"Publishers"), page="publisherlist", data="publisher")
 | 
					                                     title=_(u"Publishers"), page="publisherlist", data="publisher")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
| 
						 | 
					@ -857,8 +886,10 @@ def series_list():
 | 
				
			||||||
                .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
 | 
					                .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
 | 
				
			||||||
                .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
 | 
					                .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            thumbnails = get_thumbnails_for_book_series(entries)
 | 
				
			||||||
            return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
 | 
					            return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
 | 
				
			||||||
                                         title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view")
 | 
					                                         title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
 | 
				
			||||||
 | 
					                                         thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        abort(404)
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1235,13 +1266,16 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        offset = 0
 | 
					        offset = 0
 | 
				
			||||||
        limit_all = result_count
 | 
					        limit_all = result_count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    thumbnails = get_thumbnails_for_books(entries)
 | 
				
			||||||
    return render_title_template('search.html',
 | 
					    return render_title_template('search.html',
 | 
				
			||||||
                                 adv_searchterm=searchterm,
 | 
					                                 adv_searchterm=searchterm,
 | 
				
			||||||
                                 pagination=pagination,
 | 
					                                 pagination=pagination,
 | 
				
			||||||
                                 entries=q[offset:limit_all],
 | 
					                                 entries=q[offset:limit_all],
 | 
				
			||||||
                                 result_count=result_count,
 | 
					                                 result_count=result_count,
 | 
				
			||||||
                                 title=_(u"Advanced Search"), page="advsearch")
 | 
					                                 title=_(u"Advanced Search"),
 | 
				
			||||||
 | 
					                                 page="advsearch",
 | 
				
			||||||
 | 
					                                 thumbnails=thumbnails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@web.route("/advsearch", methods=['GET'])
 | 
					@web.route("/advsearch", methods=['GET'])
 | 
				
			||||||
| 
						 | 
					@ -1260,10 +1294,24 @@ def advanced_search_form():
 | 
				
			||||||
def get_cover(book_id):
 | 
					def get_cover(book_id):
 | 
				
			||||||
    return get_book_cover(book_id)
 | 
					    return get_book_cover(book_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@web.route("/cached-cover/<string:cache_id>")
 | 
				
			||||||
 | 
					@login_required_if_no_ano
 | 
				
			||||||
 | 
					def get_cached_cover(cache_id):
 | 
				
			||||||
 | 
					    return get_cached_book_cover(cache_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@web.route("/cached-cover-thumbnail/<string:cache_id>")
 | 
				
			||||||
 | 
					@login_required_if_no_ano
 | 
				
			||||||
 | 
					def get_cached_cover_thumbnail(cache_id):
 | 
				
			||||||
 | 
					    return get_cached_book_cover_thumbnail(cache_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@web.route("/robots.txt")
 | 
					@web.route("/robots.txt")
 | 
				
			||||||
def get_robots():
 | 
					def get_robots():
 | 
				
			||||||
    return send_from_directory(constants.STATIC_DIR, "robots.txt")
 | 
					    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>", defaults={'anyname': 'None'})
 | 
				
			||||||
@web.route("/show/<int:book_id>/<book_format>/<anyname>")
 | 
					@web.route("/show/<int:book_id>/<book_format>/<anyname>")
 | 
				
			||||||
@login_required_if_no_ano
 | 
					@login_required_if_no_ano
 | 
				
			||||||
| 
						 | 
					@ -1293,7 +1341,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)
 | 
					        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>", defaults={'anyname': 'None'})
 | 
				
			||||||
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
 | 
					@web.route("/download/<int:book_id>/<book_format>/<anyname>")
 | 
				
			||||||
@login_required_if_no_ano
 | 
					@login_required_if_no_ano
 | 
				
			||||||
| 
						 | 
					@ -1481,9 +1528,6 @@ def logout():
 | 
				
			||||||
    return redirect(url_for('web.login'))
 | 
					    return redirect(url_for('web.login'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# ################################### Users own configuration #########################################################
 | 
					# ################################### Users own configuration #########################################################
 | 
				
			||||||
def change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status):
 | 
					def change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status):
 | 
				
			||||||
    if "email" in to_save and to_save["email"] != current_user.email:
 | 
					    if "email" in to_save and to_save["email"] != current_user.email:
 | 
				
			||||||
| 
						 | 
					@ -1683,6 +1727,7 @@ def show_book(book_id):
 | 
				
			||||||
            if media_format.format.lower() in constants.EXTENSIONS_AUDIO:
 | 
					            if media_format.format.lower() in constants.EXTENSIONS_AUDIO:
 | 
				
			||||||
                audioentries.append(media_format.format.lower())
 | 
					                audioentries.append(media_format.format.lower())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        thumbnails = get_thumbnails_for_books([entries])
 | 
				
			||||||
        return render_title_template('detail.html',
 | 
					        return render_title_template('detail.html',
 | 
				
			||||||
                                     entry=entries,
 | 
					                                     entry=entries,
 | 
				
			||||||
                                     audioentries=audioentries,
 | 
					                                     audioentries=audioentries,
 | 
				
			||||||
| 
						 | 
					@ -1694,7 +1739,8 @@ def show_book(book_id):
 | 
				
			||||||
                                     is_archived=is_archived,
 | 
					                                     is_archived=is_archived,
 | 
				
			||||||
                                     kindle_list=kindle_list,
 | 
					                                     kindle_list=kindle_list,
 | 
				
			||||||
                                     reader_list=reader_list,
 | 
					                                     reader_list=reader_list,
 | 
				
			||||||
                                     page="book")
 | 
					                                     page="book",
 | 
				
			||||||
 | 
					                                     thumbnails=thumbnails)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        log.debug(u"Error opening eBook. File does not exist or file is not accessible")
 | 
					        log.debug(u"Error opening eBook. File does not exist or file is not accessible")
 | 
				
			||||||
        flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
 | 
					        flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					APScheduler>=3.6.3, <3.8.0
 | 
				
			||||||
Babel>=1.3, <2.9
 | 
					Babel>=1.3, <2.9
 | 
				
			||||||
Flask-Babel>=0.11.1,<2.1.0
 | 
					Flask-Babel>=0.11.1,<2.1.0
 | 
				
			||||||
Flask-Login>=0.3.2,<0.5.1
 | 
					Flask-Login>=0.3.2,<0.5.1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user