Merge remote-tracking branch 'origin/cover_thumbnail' into cover_thumbnail
This commit is contained in:
commit
c1ca18f7dc
2
cps.py
2
cps.py
@ -77,7 +77,7 @@ def main():
|
|||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
|
|
||||||
# Register scheduled tasks
|
# Register scheduled tasks
|
||||||
register_scheduled_tasks()
|
register_scheduled_tasks() # ToDo only reconnect if reconnect is enabled
|
||||||
register_startup_tasks()
|
register_startup_tasks()
|
||||||
|
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
@ -179,12 +179,6 @@ def get_locale():
|
|||||||
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
|
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
|
||||||
|
|
||||||
|
|
||||||
@babel.timezoneselector
|
|
||||||
def get_timezone():
|
|
||||||
user = getattr(g, 'user', None)
|
|
||||||
return user.timezone if user else None
|
|
||||||
|
|
||||||
|
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
|
||||||
|
@ -69,9 +69,9 @@ _VERSIONS.update(uploader.get_versions(False))
|
|||||||
|
|
||||||
|
|
||||||
def collect_stats():
|
def collect_stats():
|
||||||
_VERSIONS['ebook converter'] = _(converter.get_calibre_version())
|
_VERSIONS['ebook converter'] = converter.get_calibre_version()
|
||||||
_VERSIONS['unrar'] = _(converter.get_unrar_version())
|
_VERSIONS['unrar'] = converter.get_unrar_version()
|
||||||
_VERSIONS['kepubify'] = _(converter.get_kepubify_version())
|
_VERSIONS['kepubify'] = converter.get_kepubify_version()
|
||||||
return _VERSIONS
|
return _VERSIONS
|
||||||
|
|
||||||
|
|
||||||
|
91
cps/admin.py
91
cps/admin.py
@ -24,13 +24,12 @@ import os
|
|||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import operator
|
import operator
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from babel import Locale
|
from babel import Locale
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime, format_time, format_timedelta
|
||||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@ -44,7 +43,7 @@ from . import constants, logger, helper, services, cli
|
|||||||
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, \
|
||||||
kobo_sync_status, schedule
|
kobo_sync_status, schedule
|
||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
||||||
valid_email, check_username
|
valid_email, check_username, update_thumbnail_cache
|
||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .render_template import render_title_template, get_sidebar_config
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
@ -58,7 +57,8 @@ feature_support = {
|
|||||||
'goodreads': bool(services.goodreads_support),
|
'goodreads': bool(services.goodreads_support),
|
||||||
'kobo': bool(services.kobo),
|
'kobo': bool(services.kobo),
|
||||||
'updater': constants.UPDATER_AVAILABLE,
|
'updater': constants.UPDATER_AVAILABLE,
|
||||||
'gmail': bool(services.gmail)
|
'gmail': bool(services.gmail),
|
||||||
|
'scheduler': schedule.use_APScheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -169,10 +169,22 @@ def reconnect():
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/updateThumbnails", methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
@login_required
|
||||||
|
def update_thumbnails():
|
||||||
|
content = config.get_scheduled_task_settings()
|
||||||
|
if content['schedule_generate_book_covers']:
|
||||||
|
log.info("Update of Cover cache requested")
|
||||||
|
update_thumbnail_cache()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/view")
|
@admi.route("/admin/view")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def admin():
|
def admin():
|
||||||
|
locale = get_locale()
|
||||||
version = updater_thread.get_current_version_info()
|
version = updater_thread.get_current_version_info()
|
||||||
if version is False:
|
if version is False:
|
||||||
commit = _(u'Unknown')
|
commit = _(u'Unknown')
|
||||||
@ -187,15 +199,19 @@ def admin():
|
|||||||
form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
|
form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
|
||||||
elif commit[19] == '-':
|
elif commit[19] == '-':
|
||||||
form_date += timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
|
form_date += timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
|
||||||
commit = format_datetime(form_date - tz, format='short', locale=get_locale())
|
commit = format_datetime(form_date - tz, format='short', locale=locale)
|
||||||
else:
|
else:
|
||||||
commit = version['version']
|
commit = version['version']
|
||||||
|
|
||||||
all_user = ub.session.query(ub.User).all()
|
all_user = ub.session.query(ub.User).all()
|
||||||
email_settings = config.get_mail_settings()
|
email_settings = config.get_mail_settings()
|
||||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
schedule_time = format_time(time(hour=config.schedule_start_time), format="short", locale=locale)
|
||||||
|
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
|
||||||
|
schedule_duration = format_timedelta(t, format="short", threshold=.99, locale=locale)
|
||||||
|
|
||||||
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
|
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit,
|
||||||
feature_support=feature_support, kobo_support=kobo_support,
|
feature_support=feature_support, schedule_time=schedule_time,
|
||||||
|
schedule_duration=schedule_duration,
|
||||||
title=_(u"Admin page"), page="admin")
|
title=_(u"Admin page"), page="admin")
|
||||||
|
|
||||||
|
|
||||||
@ -612,6 +628,8 @@ def load_dialogtexts(element_id):
|
|||||||
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
|
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
|
||||||
elif element_id == "db_submit":
|
elif element_id == "db_submit":
|
||||||
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
||||||
|
elif element_id == "admin_refresh_cover_cache":
|
||||||
|
texts["main"] = _('Calibre-Web will search for updated Covers and update Cover Thumbnails, this may take a while?')
|
||||||
elif element_id == "btnfullsync":
|
elif element_id == "btnfullsync":
|
||||||
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database "
|
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database "
|
||||||
"to force a full sync with your Kobo Reader?")
|
"to force a full sync with your Kobo Reader?")
|
||||||
@ -1647,36 +1665,57 @@ def update_mailsettings():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def edit_scheduledtasks():
|
def edit_scheduledtasks():
|
||||||
content = config.get_scheduled_task_settings()
|
content = config.get_scheduled_task_settings()
|
||||||
return render_title_template("schedule_edit.html", config=content, title=_(u"Edit Scheduled Tasks Settings"))
|
time_field = list()
|
||||||
|
duration_field = list()
|
||||||
|
|
||||||
|
locale = get_locale()
|
||||||
|
for n in range(24):
|
||||||
|
time_field.append((n , format_time(time(hour=n), format="short", locale=locale)))
|
||||||
|
for n in range(5, 65, 5):
|
||||||
|
t = timedelta(hours=n // 60, minutes=n % 60)
|
||||||
|
duration_field.append((n, format_timedelta(t, format="short", threshold=.99, locale=locale)))
|
||||||
|
|
||||||
|
return render_title_template("schedule_edit.html", config=content, starttime=time_field, duration=duration_field, title=_(u"Edit Scheduled Tasks Settings"))
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
@admi.route("/admin/scheduledtasks", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_scheduledtasks():
|
def update_scheduledtasks():
|
||||||
|
error = False
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
_config_int(to_save, "schedule_start_time")
|
if 0 <= int(to_save.get("schedule_start_time")) <= 23:
|
||||||
_config_int(to_save, "schedule_end_time")
|
_config_int(to_save, "schedule_start_time")
|
||||||
|
else:
|
||||||
|
flash(_(u"Invalid start time for task specified"), category="error")
|
||||||
|
error = True
|
||||||
|
if 0 < int(to_save.get("schedule_duration")) <= 60:
|
||||||
|
_config_int(to_save, "schedule_duration")
|
||||||
|
else:
|
||||||
|
flash(_(u"Invalid duration for task specified"), category="error")
|
||||||
|
error = True
|
||||||
_config_checkbox(to_save, "schedule_generate_book_covers")
|
_config_checkbox(to_save, "schedule_generate_book_covers")
|
||||||
_config_checkbox(to_save, "schedule_generate_series_covers")
|
_config_checkbox(to_save, "schedule_generate_series_covers")
|
||||||
|
_config_checkbox(to_save, "schedule_reconnect")
|
||||||
|
|
||||||
try:
|
if not error:
|
||||||
config.save()
|
try:
|
||||||
flash(_(u"Scheduled tasks settings updated"), category="success")
|
config.save()
|
||||||
|
flash(_(u"Scheduled tasks settings updated"), category="success")
|
||||||
|
|
||||||
# Cancel any running tasks
|
# Cancel any running tasks
|
||||||
schedule.end_scheduled_tasks()
|
schedule.end_scheduled_tasks()
|
||||||
|
|
||||||
# Re-register tasks with new settings
|
# Re-register tasks with new settings
|
||||||
schedule.register_scheduled_tasks()
|
schedule.register_scheduled_tasks(config.schedule_reconnect)
|
||||||
except IntegrityError as ex:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("An unknown error occurred while saving scheduled tasks settings")
|
log.error("An unknown error occurred while saving scheduled tasks settings")
|
||||||
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.error("Settings DB is not Writeable")
|
log.error("Settings DB is not Writeable")
|
||||||
flash(_("Settings DB is not Writeable"), category="error")
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
return edit_scheduledtasks()
|
return edit_scheduledtasks()
|
||||||
|
|
||||||
|
@ -130,7 +130,9 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
series=loaded_metadata.series or "",
|
series=loaded_metadata.series or "",
|
||||||
series_id=loaded_metadata.issue or "",
|
series_id=loaded_metadata.issue or "",
|
||||||
languages=loaded_metadata.language,
|
languages=loaded_metadata.language,
|
||||||
publisher="")
|
publisher="",
|
||||||
|
pubdate="",
|
||||||
|
identifiers=[])
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
@ -143,4 +145,6 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="",
|
languages="",
|
||||||
publisher="")
|
publisher="",
|
||||||
|
pubdate="",
|
||||||
|
identifiers=[])
|
||||||
|
@ -142,9 +142,10 @@ class _Settings(_Base):
|
|||||||
config_allow_reverse_proxy_header_login = Column(Boolean, default=False)
|
config_allow_reverse_proxy_header_login = Column(Boolean, default=False)
|
||||||
|
|
||||||
schedule_start_time = Column(Integer, default=4)
|
schedule_start_time = Column(Integer, default=4)
|
||||||
schedule_end_time = Column(Integer, default=6)
|
schedule_duration = Column(Integer, default=10)
|
||||||
schedule_generate_book_covers = Column(Boolean, default=False)
|
schedule_generate_book_covers = Column(Boolean, default=False)
|
||||||
schedule_generate_series_covers = Column(Boolean, default=False)
|
schedule_generate_series_covers = Column(Boolean, default=False)
|
||||||
|
schedule_reconnect = Column(Boolean, default=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
@ -161,7 +161,7 @@ def selected_roles(dictionary):
|
|||||||
|
|
||||||
# :rtype: BookMeta
|
# :rtype: BookMeta
|
||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages, publisher')
|
'series_id, languages, publisher, pubdate, identifiers')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.19 Beta'}
|
STABLE_VERSION = {'version': '0.6.19 Beta'}
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from flask_babel import gettext as _
|
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
from . import config, logger
|
from . import config, logger
|
||||||
from .subproc_wrapper import process_wait
|
from .subproc_wrapper import process_wait
|
||||||
@ -26,9 +27,9 @@ from .subproc_wrapper import process_wait
|
|||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
# _() necessary to make babel aware of string for translation
|
# strings getting translated when used
|
||||||
_NOT_INSTALLED = _('not installed')
|
_NOT_INSTALLED = N_('not installed')
|
||||||
_EXECUTION_ERROR = _('Execution permissions missing')
|
_EXECUTION_ERROR = N_('Execution permissions missing')
|
||||||
|
|
||||||
|
|
||||||
def _get_command_version(path, pattern, argument=None):
|
def _get_command_version(path, pattern, argument=None):
|
||||||
|
40
cps/db.py
40
cps/db.py
@ -25,6 +25,7 @@ from datetime import datetime
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import unidecode
|
import unidecode
|
||||||
|
|
||||||
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
||||||
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
||||||
@ -903,9 +904,20 @@ class CalibreDB:
|
|||||||
.join(books_languages_link).join(Books)\
|
.join(books_languages_link).join(Books)\
|
||||||
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
||||||
.group_by(text('books_languages_link.lang_code')).all()
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
|
tags = list()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
|
tag = Category(isoLanguages.get_language_name(get_locale(), lang[0].lang_code), lang[0].lang_code)
|
||||||
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
|
tags.append([tag, lang[1]])
|
||||||
|
# Append all books without language to list
|
||||||
|
if not return_all_languages:
|
||||||
|
no_lang_count = (self.session.query(Books)
|
||||||
|
.outerjoin(books_languages_link).outerjoin(Languages)
|
||||||
|
.filter(Languages.lang_code == None)
|
||||||
|
.filter(self.common_filters())
|
||||||
|
.count())
|
||||||
|
if no_lang_count:
|
||||||
|
tags.append([Category(_("None"), "none"), no_lang_count])
|
||||||
|
return sorted(tags, key=lambda x: x[0].name, reverse=reverse_order)
|
||||||
else:
|
else:
|
||||||
if not languages:
|
if not languages:
|
||||||
languages = self.session.query(Languages) \
|
languages = self.session.query(Languages) \
|
||||||
@ -929,7 +941,10 @@ class CalibreDB:
|
|||||||
return title.strip()
|
return title.strip()
|
||||||
|
|
||||||
conn = conn or self.session.connection().connection.connection
|
conn = conn or self.session.connection().connection.connection
|
||||||
conn.create_function("title_sort", 1, _title_sort)
|
try:
|
||||||
|
conn.create_function("title_sort", 1, _title_sort)
|
||||||
|
except sqliteOperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dispose(cls):
|
def dispose(cls):
|
||||||
@ -977,3 +992,22 @@ def lcase(s):
|
|||||||
_log = logger.create()
|
_log = logger.create()
|
||||||
_log.error_or_exception(ex)
|
_log.error_or_exception(ex)
|
||||||
return s.lower()
|
return s.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class Category:
|
||||||
|
name = None
|
||||||
|
id = None
|
||||||
|
count = None
|
||||||
|
rating = None
|
||||||
|
|
||||||
|
def __init__(self, name, cat_id, rating=None):
|
||||||
|
self.name = name
|
||||||
|
self.id = cat_id
|
||||||
|
self.rating = rating
|
||||||
|
self.count = 1
|
||||||
|
|
||||||
|
'''class Count:
|
||||||
|
count = None
|
||||||
|
|
||||||
|
def __init__(self, count):
|
||||||
|
self.count = count'''
|
||||||
|
@ -25,7 +25,7 @@ from datetime import datetime
|
|||||||
import json
|
import json
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from markupsafe import escape
|
from markupsafe import escape # dependency of flask
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -35,9 +35,10 @@ except ImportError:
|
|||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
# from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||||
from . import config, get_locale, ub, db
|
from . import config, get_locale, ub, db
|
||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
@ -241,7 +242,7 @@ def delete_book_ajax(book_id, book_format):
|
|||||||
|
|
||||||
|
|
||||||
def delete_whole_book(book_id, book):
|
def delete_whole_book(book_id, book):
|
||||||
# delete book from Shelfs, Downloads, Read list
|
# delete book from shelves, Downloads, Read list
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
||||||
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
||||||
ub.delete_download(book_id)
|
ub.delete_download(book_id)
|
||||||
@ -383,7 +384,7 @@ def render_edit_book(book_id):
|
|||||||
for authr in book.authors:
|
for authr in book.authors:
|
||||||
author_names.append(authr.name.replace('|', ','))
|
author_names.append(authr.name.replace('|', ','))
|
||||||
|
|
||||||
# Option for showing convertbook button
|
# Option for showing convert_book button
|
||||||
valid_source_formats = list()
|
valid_source_formats = list()
|
||||||
allowed_conversion_formats = list()
|
allowed_conversion_formats = list()
|
||||||
kepub_possible = None
|
kepub_possible = None
|
||||||
@ -413,11 +414,11 @@ def render_edit_book(book_id):
|
|||||||
|
|
||||||
def edit_book_ratings(to_save, book):
|
def edit_book_ratings(to_save, book):
|
||||||
changed = False
|
changed = False
|
||||||
if to_save.get("rating","").strip():
|
if to_save.get("rating", "").strip():
|
||||||
old_rating = False
|
old_rating = False
|
||||||
if len(book.ratings) > 0:
|
if len(book.ratings) > 0:
|
||||||
old_rating = book.ratings[0].rating
|
old_rating = book.ratings[0].rating
|
||||||
rating_x2 = int(float(to_save.get("rating","")) * 2)
|
rating_x2 = int(float(to_save.get("rating", "")) * 2)
|
||||||
if rating_x2 != old_rating:
|
if rating_x2 != old_rating:
|
||||||
changed = True
|
changed = True
|
||||||
is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == rating_x2).first()
|
is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == rating_x2).first()
|
||||||
@ -622,8 +623,9 @@ def edit_cc_data(book_id, book, to_save, cc):
|
|||||||
'custom')
|
'custom')
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
# returns None if no file is uploaded
|
# returns None if no file is uploaded
|
||||||
# returns False if an error occours, in all other cases the ebook metadata is returned
|
# returns False if an error occurs, in all other cases the ebook metadata is returned
|
||||||
def upload_single_file(file_request, book, book_id):
|
def upload_single_file(file_request, book, book_id):
|
||||||
# Check and handle Uploaded file
|
# Check and handle Uploaded file
|
||||||
requested_file = file_request.files.get('btn-upload-format', None)
|
requested_file = file_request.files.get('btn-upload-format', None)
|
||||||
@ -676,11 +678,11 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
log.error_or_exception("Database error: {}".format(e))
|
log.error_or_exception("Database error: {}".format(e))
|
||||||
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
|
||||||
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
return False # return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
|
||||||
# Queue uploader info
|
# Queue uploader info
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title))
|
||||||
upload_text = _(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
upload_text = N_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link)
|
||||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
|
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
|
||||||
|
|
||||||
return uploader.process(
|
return uploader.process(
|
||||||
@ -688,6 +690,7 @@ def upload_single_file(file_request, book, book_id):
|
|||||||
rarExecutable=config.config_rarfile_location)
|
rarExecutable=config.config_rarfile_location)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def upload_cover(cover_request, book):
|
def upload_cover(cover_request, book):
|
||||||
requested_file = cover_request.files.get('btn-upload-cover', None)
|
requested_file = cover_request.files.get('btn-upload-cover', None)
|
||||||
if requested_file:
|
if requested_file:
|
||||||
@ -698,7 +701,7 @@ def upload_cover(cover_request, book):
|
|||||||
return False
|
return False
|
||||||
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)
|
helper.replace_cover_thumbnail_cache(book.id)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
flash(message, category="error")
|
flash(message, category="error")
|
||||||
@ -739,6 +742,7 @@ def handle_author_on_edit(book, author_name, update_stored=True):
|
|||||||
change = True
|
change = True
|
||||||
return input_authors, change, renamed
|
return input_authors, change, renamed
|
||||||
|
|
||||||
|
|
||||||
@EditBook.route("/admin/book/<int:book_id>", methods=['GET'])
|
@EditBook.route("/admin/book/<int:book_id>", methods=['GET'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
@ -754,11 +758,11 @@ def edit_book(book_id):
|
|||||||
edit_error = False
|
edit_error = False
|
||||||
|
|
||||||
# create the function for sorting...
|
# create the function for sorting...
|
||||||
try:
|
#try:
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
except sqliteOperationalError as e:
|
#except sqliteOperationalError as e:
|
||||||
log.error_or_exception(e)
|
# log.error_or_exception(e)
|
||||||
calibre_db.session.rollback()
|
# calibre_db.session.rollback()
|
||||||
|
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
# Book not found
|
# Book not found
|
||||||
@ -815,6 +819,7 @@ def edit_book(book_id):
|
|||||||
if result is True:
|
if result is True:
|
||||||
book.has_cover = 1
|
book.has_cover = 1
|
||||||
modify_date = True
|
modify_date = True
|
||||||
|
helper.replace_cover_thumbnail_cache(book.id)
|
||||||
else:
|
else:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
|
|
||||||
@ -984,8 +989,13 @@ def create_book_on_upload(modify_date, meta):
|
|||||||
# combine path and normalize path from Windows systems
|
# combine path and normalize path from Windows systems
|
||||||
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
||||||
|
|
||||||
|
try:
|
||||||
|
pubdate = datetime.strptime(meta.pubdate[:10], "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
pubdate = datetime(101, 1, 1)
|
||||||
|
|
||||||
# Calibre adds books with utc as timezone
|
# Calibre adds books with utc as timezone
|
||||||
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), pubdate,
|
||||||
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
||||||
|
|
||||||
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
|
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
|
||||||
@ -1018,6 +1028,16 @@ def create_book_on_upload(modify_date, meta):
|
|||||||
|
|
||||||
# flush content, get db_book.id available
|
# flush content, get db_book.id available
|
||||||
calibre_db.session.flush()
|
calibre_db.session.flush()
|
||||||
|
|
||||||
|
# Handle identifiers now that db_book.id is available
|
||||||
|
identifier_list = []
|
||||||
|
for type_key, type_value in meta.identifiers:
|
||||||
|
identifier_list.append(db.Identifiers(type_value, type_key, db_book.id))
|
||||||
|
modification, warning = modify_identifiers(identifier_list, db_book.identifiers, calibre_db.session)
|
||||||
|
if warning:
|
||||||
|
flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
|
||||||
|
modify_date |= modification
|
||||||
|
|
||||||
return db_book, input_authors, title_dir, renamed_authors
|
return db_book, input_authors, title_dir, renamed_authors
|
||||||
|
|
||||||
|
|
||||||
@ -1048,18 +1068,18 @@ def file_handling_on_upload(requested_file):
|
|||||||
def move_coverfile(meta, db_book):
|
def move_coverfile(meta, db_book):
|
||||||
# move cover to final directory, including book id
|
# move cover to final directory, including book id
|
||||||
if meta.cover:
|
if meta.cover:
|
||||||
coverfile = meta.cover
|
cover_file = meta.cover
|
||||||
else:
|
else:
|
||||||
coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
|
cover_file = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
|
||||||
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path)
|
new_cover_path = os.path.join(config.config_calibre_dir, db_book.path)
|
||||||
try:
|
try:
|
||||||
os.makedirs(new_coverpath, exist_ok=True)
|
os.makedirs(new_cover_path, exist_ok=True)
|
||||||
copyfile(coverfile, os.path.join(new_coverpath, "cover.jpg"))
|
copyfile(cover_file, os.path.join(new_cover_path, "cover.jpg"))
|
||||||
if meta.cover:
|
if meta.cover:
|
||||||
os.unlink(meta.cover)
|
os.unlink(meta.cover)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.error("Failed to move cover file %s: %s", new_coverpath, e)
|
log.error("Failed to move cover file %s: %s", new_cover_path, e)
|
||||||
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath,
|
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_cover_path,
|
||||||
error=e),
|
error=e),
|
||||||
category="error")
|
category="error")
|
||||||
|
|
||||||
@ -1115,8 +1135,9 @@ def upload():
|
|||||||
if error:
|
if error:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(title))
|
||||||
upload_text = _(u"File %(file)s uploaded", file=link)
|
upload_text = N_(u"File %(file)s uploaded", file=link)
|
||||||
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
|
WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
|
||||||
|
helper.add_book_to_thumbnail_cache(book_id)
|
||||||
|
|
||||||
if len(request.files.getlist("btn-upload")) < 2:
|
if len(request.files.getlist("btn-upload")) < 2:
|
||||||
if current_user.role_edit() or current_user.role_admin():
|
if current_user.role_edit() or current_user.role_admin():
|
||||||
@ -1177,7 +1198,7 @@ def edit_list_book(param):
|
|||||||
vals = request.form.to_dict()
|
vals = request.form.to_dict()
|
||||||
book = calibre_db.get_book(vals['pk'])
|
book = calibre_db.get_book(vals['pk'])
|
||||||
sort_param = ""
|
sort_param = ""
|
||||||
# ret = ""
|
ret = ""
|
||||||
try:
|
try:
|
||||||
if param == 'series_index':
|
if param == 'series_index':
|
||||||
edit_book_series_index(vals['value'], book)
|
edit_book_series_index(vals['value'], book)
|
||||||
|
22
cps/epub.py
22
cps/epub.py
@ -63,13 +63,15 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
|
|
||||||
epub_metadata = {}
|
epub_metadata = {}
|
||||||
|
|
||||||
for s in ['title', 'description', 'creator', 'language', 'subject']:
|
for s in ['title', 'description', 'creator', 'language', 'subject', 'publisher', 'date']:
|
||||||
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
||||||
if len(tmp) > 0:
|
if len(tmp) > 0:
|
||||||
if s == 'creator':
|
if s == 'creator':
|
||||||
epub_metadata[s] = ' & '.join(split_authors(tmp))
|
epub_metadata[s] = ' & '.join(split_authors(tmp))
|
||||||
elif s == 'subject':
|
elif s == 'subject':
|
||||||
epub_metadata[s] = ', '.join(tmp)
|
epub_metadata[s] = ', '.join(tmp)
|
||||||
|
elif s == 'date':
|
||||||
|
epub_metadata[s] = tmp[0][:10]
|
||||||
else:
|
else:
|
||||||
epub_metadata[s] = tmp[0]
|
epub_metadata[s] = tmp[0]
|
||||||
else:
|
else:
|
||||||
@ -78,6 +80,12 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
if epub_metadata['subject'] == 'Unknown':
|
if epub_metadata['subject'] == 'Unknown':
|
||||||
epub_metadata['subject'] = ''
|
epub_metadata['subject'] = ''
|
||||||
|
|
||||||
|
if epub_metadata['publisher'] == u'Unknown':
|
||||||
|
epub_metadata['publisher'] = ''
|
||||||
|
|
||||||
|
if epub_metadata['date'] == u'Unknown':
|
||||||
|
epub_metadata['date'] = ''
|
||||||
|
|
||||||
if epub_metadata['description'] == u'Unknown':
|
if epub_metadata['description'] == u'Unknown':
|
||||||
description = tree.xpath("//*[local-name() = 'description']/text()")
|
description = tree.xpath("//*[local-name() = 'description']/text()")
|
||||||
if len(description) > 0:
|
if len(description) > 0:
|
||||||
@ -92,6 +100,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
|
|
||||||
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
|
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
|
||||||
|
|
||||||
|
identifiers = []
|
||||||
|
for node in p.xpath('dc:identifier', namespaces=ns):
|
||||||
|
identifier_name=node.attrib.values()[-1];
|
||||||
|
identifier_value=node.text;
|
||||||
|
if identifier_name in ('uuid','calibre'):
|
||||||
|
continue;
|
||||||
|
identifiers.append( [identifier_name, identifier_value] )
|
||||||
|
|
||||||
if not epub_metadata['title']:
|
if not epub_metadata['title']:
|
||||||
title = original_file_name
|
title = original_file_name
|
||||||
else:
|
else:
|
||||||
@ -108,7 +124,9 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
||||||
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
||||||
languages=epub_metadata['language'],
|
languages=epub_metadata['language'],
|
||||||
publisher="")
|
publisher=epub_metadata['publisher'].encode('utf-8').decode('utf-8'),
|
||||||
|
pubdate=epub_metadata['date'],
|
||||||
|
identifiers=identifiers)
|
||||||
|
|
||||||
|
|
||||||
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
||||||
|
@ -77,4 +77,6 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="",
|
languages="",
|
||||||
publisher="")
|
publisher="",
|
||||||
|
pubdate="",
|
||||||
|
identifiers=[])
|
||||||
|
@ -33,6 +33,7 @@ from babel.dates import format_datetime
|
|||||||
from babel.units import format_unit
|
from babel.units import format_unit
|
||||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
from sqlalchemy.sql.expression import true, false, and_, or_, text, func
|
||||||
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
from sqlalchemy.exc import InvalidRequestError, OperationalError
|
||||||
@ -53,14 +54,14 @@ except ImportError:
|
|||||||
|
|
||||||
from . import calibre_db, cli
|
from . import calibre_db, cli
|
||||||
from .tasks.convert import TaskConvert
|
from .tasks.convert import TaskConvert
|
||||||
from . import logger, config, get_locale, db, ub, kobo_sync_status, fs
|
from . import logger, config, get_locale, db, ub, fs
|
||||||
from . import gdriveutils as gd
|
from . import gdriveutils as gd
|
||||||
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES
|
from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES
|
||||||
from .subproc_wrapper import process_wait
|
from .subproc_wrapper import process_wait
|
||||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS, STAT_ENDED, \
|
||||||
STAT_CANCELLED
|
STAT_CANCELLED
|
||||||
from .tasks.mail import TaskEmail
|
from .tasks.mail import TaskEmail
|
||||||
from .tasks.thumbnail import TaskClearCoverThumbnailCache
|
from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@ -111,9 +112,10 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Texts are not lazy translated as they are supposed to get send out as is
|
||||||
def send_test_mail(kindle_mail, user_name):
|
def send_test_mail(kindle_mail, user_name):
|
||||||
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
||||||
config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
|
config.get_mail_settings(), kindle_mail, N_(u"Test e-mail"),
|
||||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -135,7 +137,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
|||||||
attachment=None,
|
attachment=None,
|
||||||
settings=config.get_mail_settings(),
|
settings=config.get_mail_settings(),
|
||||||
recipient=e_mail,
|
recipient=e_mail,
|
||||||
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
task_message=N_(u"Registration e-mail for user: %(name)s", name=user_name),
|
||||||
text=txt
|
text=txt
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
@ -219,7 +221,7 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
|
|||||||
if entry.format.upper() == book_format.upper():
|
if entry.format.upper() == book_format.upper():
|
||||||
converted_file_name = entry.name + '.' + book_format.lower()
|
converted_file_name = entry.name + '.' + book_format.lower()
|
||||||
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book_id), escape(book.title))
|
||||||
email_text = _(u"%(book)s send to Kindle", book=link)
|
email_text = N_(u"%(book)s send to Kindle", book=link)
|
||||||
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
|
||||||
config.get_mail_settings(), kindle_mail,
|
config.get_mail_settings(), kindle_mail,
|
||||||
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
email_text, _(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
@ -715,9 +717,10 @@ def get_book_cover(book_id, resolution=None):
|
|||||||
return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
|
return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover_with_uuid(book_uuid, use_generic_cover_on_failure=True):
|
# Called only by kobo sync -> cover not found should be answered with 404 and not with default cover
|
||||||
|
def get_book_cover_with_uuid(book_uuid, resolution=None):
|
||||||
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=False, resolution=resolution)
|
||||||
|
|
||||||
|
|
||||||
def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None):
|
def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None):
|
||||||
@ -819,9 +822,6 @@ def save_cover_from_url(url, book_path):
|
|||||||
log.error("python modul advocate is not installed but is needed")
|
log.error("python modul advocate is not installed but is needed")
|
||||||
return False, _("Python modul 'advocate' is not installed but is needed for cover downloads")
|
return False, _("Python modul 'advocate' is not installed but is needed for cover downloads")
|
||||||
img.raise_for_status()
|
img.raise_for_status()
|
||||||
# # cover_processing()
|
|
||||||
# move_coverfile(meta, db_book)
|
|
||||||
|
|
||||||
return save_cover(img, book_path)
|
return save_cover(img, book_path)
|
||||||
except (socket.gaierror,
|
except (socket.gaierror,
|
||||||
requests.exceptions.HTTPError,
|
requests.exceptions.HTTPError,
|
||||||
@ -990,7 +990,7 @@ def format_runtime(runtime):
|
|||||||
# helper function to apply localize status information in tasklist entries
|
# helper function to apply localize status information in tasklist entries
|
||||||
def render_task_status(tasklist):
|
def render_task_status(tasklist):
|
||||||
renderedtasklist = list()
|
renderedtasklist = list()
|
||||||
for __, user, __, task in tasklist:
|
for __, user, __, task, __ in tasklist:
|
||||||
if user == current_user.name or current_user.role_admin():
|
if user == current_user.name or current_user.role_admin():
|
||||||
ret = {}
|
ret = {}
|
||||||
if task.start_time:
|
if task.start_time:
|
||||||
@ -1014,12 +1014,12 @@ def render_task_status(tasklist):
|
|||||||
else:
|
else:
|
||||||
ret['status'] = _(u'Unknown Status')
|
ret['status'] = _(u'Unknown Status')
|
||||||
|
|
||||||
ret['taskMessage'] = "{}: {}".format(_(task.name), task.message) if task.message else _(task.name)
|
ret['taskMessage'] = "{}: {}".format(task.name, task.message) if task.message else task.name
|
||||||
ret['progress'] = "{} %".format(int(task.progress * 100))
|
ret['progress'] = "{} %".format(int(task.progress * 100))
|
||||||
ret['user'] = escape(user) # prevent xss
|
ret['user'] = escape(user) # prevent xss
|
||||||
|
|
||||||
# Hidden fields
|
# Hidden fields
|
||||||
ret['id'] = task.id
|
ret['task_id'] = task.id
|
||||||
ret['stat'] = task.stat
|
ret['stat'] = task.stat
|
||||||
ret['is_cancellable'] = task.is_cancellable
|
ret['is_cancellable'] = task.is_cancellable
|
||||||
|
|
||||||
@ -1077,7 +1077,21 @@ def get_download_link(book_id, book_format, client):
|
|||||||
|
|
||||||
|
|
||||||
def clear_cover_thumbnail_cache(book_id):
|
def clear_cover_thumbnail_cache(book_id):
|
||||||
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id))
|
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_cover_thumbnail_cache(book_id):
|
||||||
|
WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True)
|
||||||
|
WorkerThread.add(None, TaskGenerateCoverThumbnails(book_id), hidden=True)
|
||||||
|
|
||||||
|
|
||||||
def delete_thumbnail_cache():
|
def delete_thumbnail_cache():
|
||||||
WorkerThread.add(None, TaskClearCoverThumbnailCache(-1))
|
WorkerThread.add(None, TaskClearCoverThumbnailCache(-1))
|
||||||
|
|
||||||
|
|
||||||
|
def add_book_to_thumbnail_cache(book_id):
|
||||||
|
WorkerThread.add(None, TaskGenerateCoverThumbnails(book_id), hidden=True)
|
||||||
|
|
||||||
|
|
||||||
|
def update_thumbnail_cache():
|
||||||
|
WorkerThread.add(None, TaskGenerateCoverThumbnails())
|
||||||
|
90
cps/kobo.py
90
cps/kobo.py
@ -45,7 +45,7 @@ import requests
|
|||||||
|
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from .constants import sqlalchemy_version2
|
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
from .web import download_required
|
from .web import download_required
|
||||||
@ -148,8 +148,8 @@ def HandleSyncRequest():
|
|||||||
sync_token.books_last_created = datetime.datetime.min
|
sync_token.books_last_created = datetime.datetime.min
|
||||||
sync_token.reading_state_last_modified = datetime.datetime.min
|
sync_token.reading_state_last_modified = datetime.datetime.min
|
||||||
|
|
||||||
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
||||||
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
||||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||||
|
|
||||||
new_archived_last_modified = datetime.datetime.min
|
new_archived_last_modified = datetime.datetime.min
|
||||||
@ -176,18 +176,17 @@ def HandleSyncRequest():
|
|||||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||||
ub.ArchivedBook.user_id == current_user.id))
|
ub.ArchivedBook.user_id == current_user.id))
|
||||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||||
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.order_by(ub.ArchivedBook.last_modified)
|
.order_by(ub.ArchivedBook.last_modified)
|
||||||
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
||||||
.join(ub.Shelf)
|
.join(ub.Shelf)
|
||||||
.filter(ub.Shelf.user_id == current_user.id)
|
.filter(ub.Shelf.user_id == current_user.id)
|
||||||
.filter(ub.Shelf.kobo_sync)
|
.filter(ub.Shelf.kobo_sync)
|
||||||
.distinct()
|
.distinct())
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if sqlalchemy_version2:
|
if sqlalchemy_version2:
|
||||||
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||||
@ -196,16 +195,14 @@ def HandleSyncRequest():
|
|||||||
ub.ArchivedBook.last_modified,
|
ub.ArchivedBook.last_modified,
|
||||||
ub.ArchivedBook.is_archived)
|
ub.ArchivedBook.is_archived)
|
||||||
changed_entries = (changed_entries
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||||
ub.ArchivedBook.user_id == current_user.id))
|
ub.ArchivedBook.user_id == current_user.id))
|
||||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
.order_by(db.Books.last_modified)
|
.order_by(db.Books.last_modified)
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
if sqlalchemy_version2:
|
if sqlalchemy_version2:
|
||||||
@ -215,7 +212,7 @@ def HandleSyncRequest():
|
|||||||
log.debug("Books to Sync: {}".format(len(books.all())))
|
log.debug("Books to Sync: {}".format(len(books.all())))
|
||||||
for book in books:
|
for book in books:
|
||||||
formats = [data.format for data in book.Books.data]
|
formats = [data.format for data in book.Books.data]
|
||||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
if 'KEPUB' not in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
|
|
||||||
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
||||||
@ -262,7 +259,7 @@ def HandleSyncRequest():
|
|||||||
.columns(db.Books).first()
|
.columns(db.Books).first()
|
||||||
else:
|
else:
|
||||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
||||||
.filter(ub.ArchivedBook.user_id==current_user.id) \
|
.filter(ub.ArchivedBook.user_id == current_user.id) \
|
||||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||||
|
|
||||||
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
||||||
@ -425,9 +422,9 @@ def get_author(book):
|
|||||||
author_list = []
|
author_list = []
|
||||||
autor_roles = []
|
autor_roles = []
|
||||||
for author in book.authors:
|
for author in book.authors:
|
||||||
autor_roles.append({"Name":author.name}) #.encode('unicode-escape').decode('latin-1')
|
autor_roles.append({"Name": author.name})
|
||||||
author_list.append(author.name)
|
author_list.append(author.name)
|
||||||
return {"ContributorRoles": autor_roles, "Contributors":author_list}
|
return {"ContributorRoles": autor_roles, "Contributors": author_list}
|
||||||
|
|
||||||
|
|
||||||
def get_publisher(book):
|
def get_publisher(book):
|
||||||
@ -441,6 +438,7 @@ def get_series(book):
|
|||||||
return None
|
return None
|
||||||
return book.series[0].name
|
return book.series[0].name
|
||||||
|
|
||||||
|
|
||||||
def get_seriesindex(book):
|
def get_seriesindex(book):
|
||||||
return book.series_index or 1
|
return book.series_index or 1
|
||||||
|
|
||||||
@ -485,7 +483,7 @@ def get_metadata(book):
|
|||||||
"Language": "en",
|
"Language": "en",
|
||||||
"PhoneticPronunciations": {},
|
"PhoneticPronunciations": {},
|
||||||
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
|
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
|
||||||
"Publisher": {"Imprint": "", "Name": get_publisher(book),},
|
"Publisher": {"Imprint": "", "Name": get_publisher(book), },
|
||||||
"RevisionId": book_uuid,
|
"RevisionId": book_uuid,
|
||||||
"Title": book.title,
|
"Title": book.title,
|
||||||
"WorkId": book_uuid,
|
"WorkId": book_uuid,
|
||||||
@ -504,6 +502,7 @@ def get_metadata(book):
|
|||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@ -718,7 +717,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
|||||||
*extra_filters
|
*extra_filters
|
||||||
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())
|
||||||
|
|
||||||
|
|
||||||
for shelf in shelflist:
|
for shelf in shelflist:
|
||||||
if not shelf_lib.check_shelf_view_permissions(shelf):
|
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||||
continue
|
continue
|
||||||
@ -764,6 +762,7 @@ def create_kobo_tag(shelf):
|
|||||||
)
|
)
|
||||||
return {"Tag": tag}
|
return {"Tag": tag}
|
||||||
|
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@ -808,7 +807,7 @@ def HandleStateRequest(book_uuid):
|
|||||||
book_read = kobo_reading_state.book_read_link
|
book_read = kobo_reading_state.book_read_link
|
||||||
new_book_read_status = get_ub_read_status(request_status_info["Status"])
|
new_book_read_status = get_ub_read_status(request_status_info["Status"])
|
||||||
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
|
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
|
||||||
and new_book_read_status != book_read.read_status:
|
and new_book_read_status != book_read.read_status:
|
||||||
book_read.times_started_reading += 1
|
book_read.times_started_reading += 1
|
||||||
book_read.last_time_started_reading = datetime.datetime.utcnow()
|
book_read.last_time_started_reading = datetime.datetime.utcnow()
|
||||||
book_read.read_status = new_book_read_status
|
book_read.read_status = new_book_read_status
|
||||||
@ -848,7 +847,7 @@ def get_ub_read_status(kobo_read_status):
|
|||||||
|
|
||||||
def get_or_create_reading_state(book_id):
|
def get_or_create_reading_state(book_id):
|
||||||
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id,
|
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id,
|
||||||
ub.ReadBook.user_id == int(current_user.id)).one_or_none()
|
ub.ReadBook.user_id == int(current_user.id)).one_or_none()
|
||||||
if not book_read:
|
if not book_read:
|
||||||
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
||||||
if not book_read.kobo_reading_state:
|
if not book_read.kobo_reading_state:
|
||||||
@ -912,13 +911,12 @@ def get_current_bookmark_response(current_bookmark):
|
|||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@kobo.route("/<book_uuid>/<width>/<height>/<isGreyscale>/image.jpg", defaults={'Quality': ""})
|
@kobo.route("/<book_uuid>/<width>/<height>/<isGreyscale>/image.jpg", defaults={'Quality': ""})
|
||||||
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
|
@kobo.route("/<book_uuid>/<width>/<height>/<Quality>/<isGreyscale>/image.jpg")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleCoverImageRequest(book_uuid, width, height,Quality, isGreyscale):
|
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
||||||
book_cover = helper.get_book_cover_with_uuid(
|
book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=COVER_THUMBNAIL_SMALL)
|
||||||
book_uuid, use_generic_cover_on_failure=False
|
|
||||||
)
|
|
||||||
if not book_cover:
|
if not book_cover:
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid)
|
log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid)
|
||||||
@ -991,8 +989,8 @@ def handle_getests():
|
|||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
else:
|
else:
|
||||||
testkey = request.headers.get("X-Kobo-userkey","")
|
testkey = request.headers.get("X-Kobo-userkey", "")
|
||||||
return make_response(jsonify({"Result": "Success", "TestKey":testkey, "Tests": {}}))
|
return make_response(jsonify({"Result": "Success", "TestKey": testkey, "Tests": {}}))
|
||||||
|
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@ -1022,7 +1020,7 @@ def make_calibre_web_auth_response():
|
|||||||
content = request.get_json()
|
content = request.get_json()
|
||||||
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
AccessToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||||
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
RefreshToken = base64.b64encode(os.urandom(24)).decode('utf-8')
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"AccessToken": AccessToken,
|
"AccessToken": AccessToken,
|
||||||
@ -1160,14 +1158,16 @@ def NATIVE_KOBO_RESOURCES():
|
|||||||
"eula_page": "https://www.kobo.com/termsofuse?style=onestore",
|
"eula_page": "https://www.kobo.com/termsofuse?style=onestore",
|
||||||
"exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange",
|
"exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange",
|
||||||
"external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}",
|
"external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}",
|
||||||
"facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/",
|
"facebook_sso_page":
|
||||||
|
"https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/",
|
||||||
"featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}",
|
"featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}",
|
||||||
"featured_lists": "https://storeapi.kobo.com/v1/products/featured",
|
"featured_lists": "https://storeapi.kobo.com/v1/products/featured",
|
||||||
"free_books_page": {
|
"free_books_page": {
|
||||||
"EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks",
|
"EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks",
|
||||||
"FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits",
|
"FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits",
|
||||||
"IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti",
|
"IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti",
|
||||||
"NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
"NL": "https://www.kobo.com/{region}/{language}/"
|
||||||
|
"List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg",
|
||||||
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis",
|
"PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis",
|
||||||
},
|
},
|
||||||
"fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback",
|
"fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback",
|
||||||
@ -1192,7 +1192,8 @@ def NATIVE_KOBO_RESOURCES():
|
|||||||
"library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}",
|
"library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}",
|
||||||
"library_sync": "https://storeapi.kobo.com/v1/library/sync",
|
"library_sync": "https://storeapi.kobo.com/v1/library/sync",
|
||||||
"love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints",
|
"love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints",
|
||||||
"love_points_redemption_page": "https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}",
|
"love_points_redemption_page":
|
||||||
|
"https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}",
|
||||||
"magazine_landing_page": "https://store.kobobooks.com/emagazines",
|
"magazine_landing_page": "https://store.kobobooks.com/emagazines",
|
||||||
"notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration",
|
"notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration",
|
||||||
"oauth_host": "https://oauth.kobo.com",
|
"oauth_host": "https://oauth.kobo.com",
|
||||||
@ -1208,7 +1209,8 @@ def NATIVE_KOBO_RESOURCES():
|
|||||||
"product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations",
|
"product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations",
|
||||||
"product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews",
|
"product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews",
|
||||||
"products": "https://storeapi.kobo.com/v1/products",
|
"products": "https://storeapi.kobo.com/v1/products",
|
||||||
"provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/",
|
"provider_external_sign_in_page":
|
||||||
|
"https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/",
|
||||||
"purchase_buy": "https://www.kobo.com/checkout/createpurchase/",
|
"purchase_buy": "https://www.kobo.com/checkout/createpurchase/",
|
||||||
"purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}",
|
"purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}",
|
||||||
"quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout",
|
"quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout",
|
||||||
|
@ -19,11 +19,13 @@
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup as BS # requirement
|
from bs4 import BeautifulSoup as BS # requirement
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cchardet #optional for better speed
|
import cchardet #optional for better speed
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
from cps import logger
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
import cps.logger as logger
|
import cps.logger as logger
|
||||||
|
|
||||||
@ -31,6 +33,9 @@ import cps.logger as logger
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
class Amazon(Metadata):
|
class Amazon(Metadata):
|
||||||
__name__ = "Amazon"
|
__name__ = "Amazon"
|
||||||
__id__ = "amazon"
|
__id__ = "amazon"
|
||||||
@ -49,17 +54,21 @@ class Amazon(Metadata):
|
|||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||||
):
|
) -> Optional[List[MetaRecord]]:
|
||||||
#timer=time()
|
#timer=time()
|
||||||
def inner(link, index) -> [dict, int]:
|
def inner(link, index) -> [dict, int]:
|
||||||
try:
|
with self.session as session:
|
||||||
with self.session as session:
|
try:
|
||||||
r = session.get(f"https://www.amazon.com{link}")
|
r = session.get(f"https://www.amazon.com/{link}")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
long_soup = BS(r.text, "lxml") #~4sec :/
|
except Exception as ex:
|
||||||
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
log.warning(ex)
|
||||||
if soup2 is None:
|
return
|
||||||
return
|
long_soup = BS(r.text, "lxml") #~4sec :/
|
||||||
|
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-books-ppd_csm_instrumentation_wrapper"})
|
||||||
|
if soup2 is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
match = MetaRecord(
|
match = MetaRecord(
|
||||||
title = "",
|
title = "",
|
||||||
authors = "",
|
authors = "",
|
||||||
@ -104,27 +113,29 @@ class Amazon(Metadata):
|
|||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
match.cover = ""
|
match.cover = ""
|
||||||
return match, index
|
return match, index
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error_or_exception(e)
|
log.error_or_exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
val = list()
|
val = list()
|
||||||
try:
|
if self.active:
|
||||||
if self.active:
|
try:
|
||||||
results = self.session.get(
|
results = self.session.get(
|
||||||
f"https://www.amazon.com/s?k={query.replace(' ', '+')}"
|
f"https://www.amazon.com/s?k={query.replace(' ', '+')}&i=digital-text&sprefix={query.replace(' ', '+')}"
|
||||||
f"&i=digital-text&sprefix={query.replace(' ', '+')}"
|
|
||||||
f"%2Cdigital-text&ref=nb_sb_noss",
|
f"%2Cdigital-text&ref=nb_sb_noss",
|
||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
results.raise_for_status()
|
results.raise_for_status()
|
||||||
soup = BS(results.text, 'html.parser')
|
except requests.exceptions.HTTPError as e:
|
||||||
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
log.error_or_exception(e)
|
||||||
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
return None
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
except Exception as e:
|
||||||
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:5])}
|
log.warning(e)
|
||||||
val = list(map(lambda x: x.result(), concurrent.futures.as_completed(fut)))
|
return None
|
||||||
result = list(filter(lambda x: x, val))
|
soup = BS(results.text, 'html.parser')
|
||||||
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
|
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
|
||||||
except requests.exceptions.HTTPError as e:
|
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
|
||||||
log.error_or_exception(e)
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
return []
|
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:5])}
|
||||||
|
val = list(map(lambda x : x.result() ,concurrent.futures.as_completed(fut)))
|
||||||
|
result = list(filter(lambda x: x, val))
|
||||||
|
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
|
||||||
|
@ -21,8 +21,11 @@ from typing import Dict, List, Optional
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from cps import logger
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
class ComicVine(Metadata):
|
class ComicVine(Metadata):
|
||||||
__name__ = "ComicVine"
|
__name__ = "ComicVine"
|
||||||
@ -46,10 +49,15 @@ class ComicVine(Metadata):
|
|||||||
if title_tokens:
|
if title_tokens:
|
||||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||||
query = "%20".join(tokens)
|
query = "%20".join(tokens)
|
||||||
result = requests.get(
|
try:
|
||||||
f"{ComicVine.BASE_URL}{query}{ComicVine.QUERY_PARAMS}",
|
result = requests.get(
|
||||||
headers=ComicVine.HEADERS,
|
f"{ComicVine.BASE_URL}{query}{ComicVine.QUERY_PARAMS}",
|
||||||
)
|
headers=ComicVine.HEADERS,
|
||||||
|
)
|
||||||
|
result.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
for result in result.json()["results"]:
|
for result in result.json()["results"]:
|
||||||
match = self._parse_search_result(
|
match = self._parse_search_result(
|
||||||
result=result, generic_cover=generic_cover, locale=locale
|
result=result, generic_cover=generic_cover, locale=locale
|
||||||
|
206
cps/metadata_provider/douban.py
Normal file
206
cps/metadata_provider/douban.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2022 xlivevil
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
import re
|
||||||
|
from concurrent import futures
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from html2text import HTML2Text
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from cps import logger
|
||||||
|
from cps.services.Metadata import Metadata, MetaRecord, MetaSourceInfo
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def html2text(html: str) -> str:
|
||||||
|
|
||||||
|
h2t = HTML2Text()
|
||||||
|
h2t.body_width = 0
|
||||||
|
h2t.single_line_break = True
|
||||||
|
h2t.emphasis_mark = "*"
|
||||||
|
return h2t.handle(html)
|
||||||
|
|
||||||
|
|
||||||
|
class Douban(Metadata):
|
||||||
|
__name__ = "豆瓣"
|
||||||
|
__id__ = "douban"
|
||||||
|
DESCRIPTION = "豆瓣"
|
||||||
|
META_URL = "https://book.douban.com/"
|
||||||
|
SEARCH_URL = "https://www.douban.com/j/search"
|
||||||
|
|
||||||
|
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
|
||||||
|
AUTHORS_PATTERN = re.compile(r"作者|译者")
|
||||||
|
PUBLISHER_PATTERN = re.compile(r"出版社")
|
||||||
|
SUBTITLE_PATTERN = re.compile(r"副标题")
|
||||||
|
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
|
||||||
|
SERIES_PATTERN = re.compile(r"丛书")
|
||||||
|
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
|
||||||
|
|
||||||
|
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
|
||||||
|
COVER_XPATH = "//a[@class='nbg']"
|
||||||
|
INFO_XPATH = "//*[@id='info']//span[@class='pl']"
|
||||||
|
TAGS_XPATH = "//a[contains(@class, 'tag')]"
|
||||||
|
DESCRIPTION_XPATH = "//div[@id='link-report']//div[@class='intro']"
|
||||||
|
RATING_XPATH = "//div[@class='rating_self clearfix']/strong"
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers = {
|
||||||
|
'user-agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
|
||||||
|
}
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||||
|
) -> Optional[List[MetaRecord]]:
|
||||||
|
if self.active:
|
||||||
|
log.debug(f"starting search {query} on douban")
|
||||||
|
if title_tokens := list(
|
||||||
|
self.get_title_tokens(query, strip_joiners=False)
|
||||||
|
):
|
||||||
|
query = "+".join(title_tokens)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = self.session.get(
|
||||||
|
self.SEARCH_URL, params={"cat": 1001, "q": query}
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
results = r.json()
|
||||||
|
if results["total"] == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
book_id_list = [
|
||||||
|
self.ID_PATTERN.search(item).group("id")
|
||||||
|
for item in results["items"][:10] if self.ID_PATTERN.search(item)
|
||||||
|
]
|
||||||
|
|
||||||
|
with futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
|
|
||||||
|
fut = [
|
||||||
|
executor.submit(self._parse_single_book, book_id, generic_cover)
|
||||||
|
for book_id in book_id_list
|
||||||
|
]
|
||||||
|
|
||||||
|
val = [
|
||||||
|
future.result()
|
||||||
|
for future in futures.as_completed(fut) if future.result()
|
||||||
|
]
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
def _parse_single_book(
|
||||||
|
self, id: str, generic_cover: str = ""
|
||||||
|
) -> Optional[MetaRecord]:
|
||||||
|
url = f"https://book.douban.com/subject/{id}/"
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = self.session.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
match = MetaRecord(
|
||||||
|
id=id,
|
||||||
|
title="",
|
||||||
|
authors=[],
|
||||||
|
url=url,
|
||||||
|
source=MetaSourceInfo(
|
||||||
|
id=self.__id__,
|
||||||
|
description=self.DESCRIPTION,
|
||||||
|
link=self.META_URL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
html = etree.HTML(r.content.decode("utf8"))
|
||||||
|
|
||||||
|
match.title = html.xpath(self.TITTLE_XPATH)[0].text
|
||||||
|
match.cover = html.xpath(self.COVER_XPATH)[0].attrib["href"] or generic_cover
|
||||||
|
try:
|
||||||
|
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
|
||||||
|
except Exception:
|
||||||
|
rating_num = 0
|
||||||
|
match.rating = int(-1 * rating_num // 2 * -1) if rating_num else 0
|
||||||
|
|
||||||
|
tag_elements = html.xpath(self.TAGS_XPATH)
|
||||||
|
if len(tag_elements):
|
||||||
|
match.tags = [tag_element.text for tag_element in tag_elements]
|
||||||
|
|
||||||
|
description_element = html.xpath(self.DESCRIPTION_XPATH)
|
||||||
|
if len(description_element):
|
||||||
|
match.description = html2text(etree.tostring(
|
||||||
|
description_element[-1], encoding="utf8").decode("utf8"))
|
||||||
|
|
||||||
|
info = html.xpath(self.INFO_XPATH)
|
||||||
|
|
||||||
|
for element in info:
|
||||||
|
text = element.text
|
||||||
|
if self.AUTHORS_PATTERN.search(text):
|
||||||
|
next = element.getnext()
|
||||||
|
while next is not None and next.tag != "br":
|
||||||
|
match.authors.append(next.text)
|
||||||
|
next = next.getnext()
|
||||||
|
elif self.PUBLISHER_PATTERN.search(text):
|
||||||
|
match.publisher = element.tail.strip()
|
||||||
|
elif self.SUBTITLE_PATTERN.search(text):
|
||||||
|
match.title = f'{match.title}:' + element.tail.strip()
|
||||||
|
elif self.PUBLISHED_DATE_PATTERN.search(text):
|
||||||
|
match.publishedDate = self._clean_date(element.tail.strip())
|
||||||
|
elif self.SUBTITLE_PATTERN.search(text):
|
||||||
|
match.series = element.getnext().text
|
||||||
|
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
|
||||||
|
match.identifiers[i_type.group()] = element.tail.strip()
|
||||||
|
|
||||||
|
return match
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_date(self, date: str) -> str:
|
||||||
|
"""
|
||||||
|
Clean up the date string to be in the format YYYY-MM-DD
|
||||||
|
|
||||||
|
Examples of possible patterns:
|
||||||
|
'2014-7-16', '1988年4月', '1995-04', '2021-8', '2020-12-1', '1996年',
|
||||||
|
'1972', '2004/11/01', '1959年3月北京第1版第1印'
|
||||||
|
"""
|
||||||
|
year = date[:4]
|
||||||
|
moon = "01"
|
||||||
|
day = "01"
|
||||||
|
|
||||||
|
if len(date) > 5:
|
||||||
|
digit = []
|
||||||
|
ls = []
|
||||||
|
for i in range(5, len(date)):
|
||||||
|
if date[i].isdigit():
|
||||||
|
digit.append(date[i])
|
||||||
|
elif digit:
|
||||||
|
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
||||||
|
digit = []
|
||||||
|
if digit:
|
||||||
|
ls.append("".join(digit) if len(digit)==2 else f"0{digit[0]}")
|
||||||
|
|
||||||
|
moon = ls[0]
|
||||||
|
if len(ls)>1:
|
||||||
|
day = ls[1]
|
||||||
|
|
||||||
|
return f"{year}-{moon}-{day}"
|
@ -22,9 +22,12 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from cps import logger
|
||||||
from cps.isoLanguages import get_lang3, get_language_name
|
from cps.isoLanguages import get_lang3, get_language_name
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
class Google(Metadata):
|
class Google(Metadata):
|
||||||
__name__ = "Google"
|
__name__ = "Google"
|
||||||
@ -45,7 +48,12 @@ class Google(Metadata):
|
|||||||
if title_tokens:
|
if title_tokens:
|
||||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||||
query = "+".join(tokens)
|
query = "+".join(tokens)
|
||||||
results = requests.get(Google.SEARCH_URL + query)
|
try:
|
||||||
|
results = requests.get(Google.SEARCH_URL + query)
|
||||||
|
results.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
for result in results.json().get("items", []):
|
for result in results.json().get("items", []):
|
||||||
val.append(
|
val.append(
|
||||||
self._parse_search_result(
|
self._parse_search_result(
|
||||||
|
@ -27,9 +27,12 @@ from html2text import HTML2Text
|
|||||||
from lxml.html import HtmlElement, fromstring, tostring
|
from lxml.html import HtmlElement, fromstring, tostring
|
||||||
from markdown2 import Markdown
|
from markdown2 import Markdown
|
||||||
|
|
||||||
|
from cps import logger
|
||||||
from cps.isoLanguages import get_language_name
|
from cps.isoLanguages import get_language_name
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
SYMBOLS_TO_TRANSLATE = (
|
SYMBOLS_TO_TRANSLATE = (
|
||||||
"öÖüÜóÓőŐúÚéÉáÁűŰíÍąĄćĆęĘłŁńŃóÓśŚźŹżŻ",
|
"öÖüÜóÓőŐúÚéÉáÁűŰíÍąĄćĆęĘłŁńŃóÓśŚźŹżŻ",
|
||||||
"oOuUoOoOuUeEaAuUiIaAcCeElLnNoOsSzZzZ",
|
"oOuUoOoOuUeEaAuUiIaAcCeElLnNoOsSzZzZ",
|
||||||
@ -112,20 +115,23 @@ class LubimyCzytac(Metadata):
|
|||||||
self, query: str, generic_cover: str = "", locale: str = "en"
|
self, query: str, generic_cover: str = "", locale: str = "en"
|
||||||
) -> Optional[List[MetaRecord]]:
|
) -> Optional[List[MetaRecord]]:
|
||||||
if self.active:
|
if self.active:
|
||||||
result = requests.get(self._prepare_query(title=query))
|
try:
|
||||||
if result.text:
|
result = requests.get(self._prepare_query(title=query))
|
||||||
root = fromstring(result.text)
|
result.raise_for_status()
|
||||||
lc_parser = LubimyCzytacParser(root=root, metadata=self)
|
except Exception as e:
|
||||||
matches = lc_parser.parse_search_results()
|
log.warning(e)
|
||||||
if matches:
|
return None
|
||||||
with ThreadPool(processes=10) as pool:
|
root = fromstring(result.text)
|
||||||
final_matches = pool.starmap(
|
lc_parser = LubimyCzytacParser(root=root, metadata=self)
|
||||||
lc_parser.parse_single_book,
|
matches = lc_parser.parse_search_results()
|
||||||
[(match, generic_cover, locale) for match in matches],
|
if matches:
|
||||||
)
|
with ThreadPool(processes=10) as pool:
|
||||||
return final_matches
|
final_matches = pool.starmap(
|
||||||
return matches
|
lc_parser.parse_single_book,
|
||||||
return []
|
[(match, generic_cover, locale) for match in matches],
|
||||||
|
)
|
||||||
|
return final_matches
|
||||||
|
return matches
|
||||||
|
|
||||||
def _prepare_query(self, title: str) -> str:
|
def _prepare_query(self, title: str) -> str:
|
||||||
query = ""
|
query = ""
|
||||||
@ -202,7 +208,12 @@ class LubimyCzytacParser:
|
|||||||
def parse_single_book(
|
def parse_single_book(
|
||||||
self, match: MetaRecord, generic_cover: str, locale: str
|
self, match: MetaRecord, generic_cover: str, locale: str
|
||||||
) -> MetaRecord:
|
) -> MetaRecord:
|
||||||
response = requests.get(match.url)
|
try:
|
||||||
|
response = requests.get(match.url)
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
self.root = fromstring(response.text)
|
self.root = fromstring(response.text)
|
||||||
match.cover = self._parse_cover(generic_cover=generic_cover)
|
match.cover = self._parse_cover(generic_cover=generic_cover)
|
||||||
match.description = self._parse_description()
|
match.description = self._parse_description()
|
||||||
|
@ -28,8 +28,12 @@ try:
|
|||||||
except FakeUserAgentError:
|
except FakeUserAgentError:
|
||||||
raise ImportError("No module named 'scholarly'")
|
raise ImportError("No module named 'scholarly'")
|
||||||
|
|
||||||
|
from cps import logger
|
||||||
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
class scholar(Metadata):
|
class scholar(Metadata):
|
||||||
__name__ = "Google Scholar"
|
__name__ = "Google Scholar"
|
||||||
__id__ = "googlescholar"
|
__id__ = "googlescholar"
|
||||||
@ -44,7 +48,11 @@ class scholar(Metadata):
|
|||||||
if title_tokens:
|
if title_tokens:
|
||||||
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
|
||||||
query = " ".join(tokens)
|
query = " ".join(tokens)
|
||||||
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
|
try:
|
||||||
|
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
return None
|
||||||
for result in scholar_gen:
|
for result in scholar_gen:
|
||||||
match = self._parse_search_result(
|
match = self._parse_search_result(
|
||||||
result=result, generic_cover="", locale=locale
|
result=result, generic_cover="", locale=locale
|
||||||
|
@ -19,38 +19,39 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from . import config, constants
|
from . import config, constants
|
||||||
from .services.background_scheduler import BackgroundScheduler
|
from .services.background_scheduler import BackgroundScheduler, use_APScheduler
|
||||||
from .tasks.database import TaskReconnectDatabase
|
from .tasks.database import TaskReconnectDatabase
|
||||||
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails
|
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
def get_scheduled_tasks(reconnect=True):
|
def get_scheduled_tasks(reconnect=True):
|
||||||
tasks = list()
|
tasks = list()
|
||||||
|
# config.schedule_reconnect or
|
||||||
# Reconnect Calibre database (metadata.db)
|
# Reconnect Calibre database (metadata.db)
|
||||||
if reconnect:
|
if reconnect:
|
||||||
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect'])
|
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
|
||||||
|
|
||||||
# Generate all missing book cover thumbnails
|
# Generate all missing book cover thumbnails
|
||||||
if config.schedule_generate_book_covers:
|
if config.schedule_generate_book_covers:
|
||||||
tasks.append([lambda: TaskGenerateCoverThumbnails(), 'generate book covers'])
|
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
|
||||||
|
tasks.append([lambda: TaskGenerateCoverThumbnails(), 'generate book covers', False])
|
||||||
|
|
||||||
# Generate all missing series thumbnails
|
# Generate all missing series thumbnails
|
||||||
if config.schedule_generate_series_covers:
|
if config.schedule_generate_series_covers:
|
||||||
tasks.append([lambda: TaskGenerateSeriesThumbnails(), 'generate book covers'])
|
tasks.append([lambda: TaskGenerateSeriesThumbnails(), 'generate book covers', False])
|
||||||
|
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
def end_scheduled_tasks():
|
def end_scheduled_tasks():
|
||||||
worker = WorkerThread.get_instance()
|
worker = WorkerThread.get_instance()
|
||||||
for __, __, __, task in worker.tasks:
|
for __, __, __, task, __ in worker.tasks:
|
||||||
if task.scheduled and task.is_cancellable:
|
if task.scheduled and task.is_cancellable:
|
||||||
worker.end_task(task.id)
|
worker.end_task(task.id)
|
||||||
|
|
||||||
|
|
||||||
def register_scheduled_tasks():
|
def register_scheduled_tasks(reconnect=True):
|
||||||
scheduler = BackgroundScheduler()
|
scheduler = BackgroundScheduler()
|
||||||
|
|
||||||
if scheduler:
|
if scheduler:
|
||||||
@ -58,16 +59,17 @@ def register_scheduled_tasks():
|
|||||||
scheduler.remove_all_jobs()
|
scheduler.remove_all_jobs()
|
||||||
|
|
||||||
start = config.schedule_start_time
|
start = config.schedule_start_time
|
||||||
end = config.schedule_end_time
|
duration = config.schedule_duration
|
||||||
|
|
||||||
# Register scheduled tasks
|
# Register scheduled tasks
|
||||||
if start != end:
|
scheduler.schedule_tasks(tasks=get_scheduled_tasks(), trigger='cron', hour=start)
|
||||||
scheduler.schedule_tasks(tasks=get_scheduled_tasks(), trigger='cron', hour=start)
|
end_time = calclulate_end_time(start, duration)
|
||||||
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end)
|
scheduler.schedule(func=end_scheduled_tasks, trigger='cron', name="end scheduled task", hour=end_time.hour,
|
||||||
|
minute=end_time.minute)
|
||||||
|
|
||||||
# Kick-off tasks, if they should currently be running
|
# Kick-off tasks, if they should currently be running
|
||||||
if should_task_be_running(start, end):
|
if should_task_be_running(start, duration):
|
||||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(reconnect))
|
||||||
|
|
||||||
|
|
||||||
def register_startup_tasks():
|
def register_startup_tasks():
|
||||||
@ -75,14 +77,21 @@ def register_startup_tasks():
|
|||||||
|
|
||||||
if scheduler:
|
if scheduler:
|
||||||
start = config.schedule_start_time
|
start = config.schedule_start_time
|
||||||
end = config.schedule_end_time
|
duration = config.schedule_duration
|
||||||
|
|
||||||
# Run scheduled tasks immediately for development and testing
|
# Run scheduled tasks immediately for development and testing
|
||||||
# Ignore tasks that should currently be running, as these will be added when registering scheduled tasks
|
# Ignore tasks that should currently be running, as these will be added when registering scheduled tasks
|
||||||
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, end):
|
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, duration):
|
||||||
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
|
||||||
|
|
||||||
|
|
||||||
def should_task_be_running(start, end):
|
def should_task_be_running(start, duration):
|
||||||
now = datetime.datetime.now().hour
|
now = datetime.datetime.now()
|
||||||
return (start < end and start <= now < end) or (end < start <= now or now < end)
|
start_time = datetime.datetime.now().replace(hour=start, minute=0, second=0, microsecond=0)
|
||||||
|
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||||
|
return start_time < now < end_time
|
||||||
|
|
||||||
|
def calclulate_end_time(start, duration):
|
||||||
|
start_time = datetime.datetime.now().replace(hour=start, minute=0)
|
||||||
|
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ for f in modules:
|
|||||||
try:
|
try:
|
||||||
importlib.import_module("cps.metadata_provider." + a)
|
importlib.import_module("cps.metadata_provider." + a)
|
||||||
new_list.append(a)
|
new_list.append(a)
|
||||||
except ImportError as e:
|
except (ImportError, IndentationError, SyntaxError) as e:
|
||||||
log.error("Import error for metadata source: {} - {}".format(a, e))
|
log.error("Import error for metadata source: {} - {}".format(a, e))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -138,6 +138,6 @@ def metadata_search():
|
|||||||
if active.get(c.__id__, True)
|
if active.get(c.__id__, True)
|
||||||
}
|
}
|
||||||
for future in concurrent.futures.as_completed(meta):
|
for future in concurrent.futures.as_completed(meta):
|
||||||
data.extend([asdict(x) for x in future.result()])
|
data.extend([asdict(x) for x in future.result() if x])
|
||||||
# log.info({'Time elapsed {}'.format(current_milli_time()-start)})
|
# log.info({'Time elapsed {}'.format(current_milli_time()-start)})
|
||||||
return Response(json.dumps(data), mimetype="application/json")
|
return Response(json.dumps(data), mimetype="application/json")
|
||||||
|
@ -52,32 +52,32 @@ class BackgroundScheduler:
|
|||||||
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args)
|
return self.scheduler.add_job(func=func, trigger=trigger, name=name, **trigger_args)
|
||||||
|
|
||||||
# Expects a lambda expression for the task
|
# Expects a lambda expression for the task
|
||||||
def schedule_task(self, task, user=None, name=None, trigger='cron', **trigger_args):
|
def schedule_task(self, task, user=None, name=None, hidden=False, trigger='cron', **trigger_args):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
def scheduled_task():
|
def scheduled_task():
|
||||||
worker_task = task()
|
worker_task = task()
|
||||||
worker_task.scheduled = True
|
worker_task.scheduled = True
|
||||||
WorkerThread.add(user, worker_task)
|
WorkerThread.add(user, worker_task, hidden=hidden)
|
||||||
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args)
|
return self.schedule(func=scheduled_task, trigger=trigger, name=name, **trigger_args)
|
||||||
|
|
||||||
# Expects a list of lambda expressions for the tasks
|
# Expects a list of lambda expressions for the tasks
|
||||||
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
|
def schedule_tasks(self, tasks, user=None, trigger='cron', **trigger_args):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], **trigger_args)
|
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2], **trigger_args)
|
||||||
|
|
||||||
# Expects a lambda expression for the task
|
# Expects a lambda expression for the task
|
||||||
def schedule_task_immediately(self, task, user=None, name=None):
|
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
def immediate_task():
|
def immediate_task():
|
||||||
WorkerThread.add(user, task())
|
WorkerThread.add(user, task(), hidden)
|
||||||
return self.schedule(func=immediate_task, trigger='date', name=name)
|
return self.schedule(func=immediate_task, trigger='date', name=name)
|
||||||
|
|
||||||
# Expects a list of lambda expressions for the tasks
|
# Expects a list of lambda expressions for the tasks
|
||||||
def schedule_tasks_immediately(self, tasks, user=None):
|
def schedule_tasks_immediately(self, tasks, user=None):
|
||||||
if use_APScheduler:
|
if use_APScheduler:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
self.schedule_task_immediately(task[0], user, name="immediately " + task[1])
|
self.schedule_task_immediately(task[0], user, name="immediately " + task[1], hidden=task[2])
|
||||||
|
|
||||||
# Remove all jobs
|
# Remove all jobs
|
||||||
def remove_all_jobs(self):
|
def remove_all_jobs(self):
|
||||||
|
@ -43,7 +43,7 @@ STAT_CANCELLED = 5
|
|||||||
# Only retain this many tasks in dequeued list
|
# Only retain this many tasks in dequeued list
|
||||||
TASK_CLEANUP_TRIGGER = 20
|
TASK_CLEANUP_TRIGGER = 20
|
||||||
|
|
||||||
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task')
|
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task, hidden')
|
||||||
|
|
||||||
|
|
||||||
def _get_main_thread():
|
def _get_main_thread():
|
||||||
@ -84,7 +84,7 @@ class WorkerThread(threading.Thread):
|
|||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, user, task):
|
def add(cls, user, task, hidden=False):
|
||||||
ins = cls.get_instance()
|
ins = cls.get_instance()
|
||||||
ins.num += 1
|
ins.num += 1
|
||||||
username = user if user is not None else 'System'
|
username = user if user is not None else 'System'
|
||||||
@ -94,6 +94,7 @@ class WorkerThread(threading.Thread):
|
|||||||
user=username,
|
user=username,
|
||||||
added=datetime.now(),
|
added=datetime.now(),
|
||||||
task=task,
|
task=task,
|
||||||
|
hidden=hidden
|
||||||
))
|
))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -114,10 +115,10 @@ class WorkerThread(threading.Thread):
|
|||||||
if delta > TASK_CLEANUP_TRIGGER:
|
if delta > TASK_CLEANUP_TRIGGER:
|
||||||
ret = alive
|
ret = alive
|
||||||
else:
|
else:
|
||||||
# otherwise, lop off the oldest dead tasks until we hit the target trigger
|
# otherwise, loop off the oldest dead tasks until we hit the target trigger
|
||||||
ret = sorted(dead, key=lambda x: x.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
|
ret = sorted(dead, key=lambda y: y.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
|
||||||
|
|
||||||
self.dequeued = sorted(ret, key=lambda x: x.num)
|
self.dequeued = sorted(ret, key=lambda y: y.num)
|
||||||
|
|
||||||
# Main thread loop starting the different tasks
|
# Main thread loop starting the different tasks
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -144,18 +145,18 @@ class WorkerThread(threading.Thread):
|
|||||||
|
|
||||||
# sometimes tasks (like Upload) don't actually have work to do and are created as already finished
|
# sometimes tasks (like Upload) don't actually have work to do and are created as already finished
|
||||||
if item.task.stat is STAT_WAITING:
|
if item.task.stat is STAT_WAITING:
|
||||||
# CalibreTask.start() should wrap all exceptions in it's own error handling
|
# CalibreTask.start() should wrap all exceptions in its own error handling
|
||||||
item.task.start(self)
|
item.task.start(self)
|
||||||
|
|
||||||
# remove self_cleanup tasks from list
|
# remove self_cleanup tasks and hidden "System Tasks" from list
|
||||||
if item.task.self_cleanup:
|
if item.task.self_cleanup or item.hidden:
|
||||||
self.dequeued.remove(item)
|
self.dequeued.remove(item)
|
||||||
|
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
def end_task(self, task_id):
|
def end_task(self, task_id):
|
||||||
ins = self.get_instance()
|
ins = self.get_instance()
|
||||||
for __, __, __, task in ins.tasks:
|
for __, __, __, task, __ in ins.tasks:
|
||||||
if str(task.id) == str(task_id) and task.is_cancellable:
|
if str(task.id) == str(task_id) and task.is_cancellable:
|
||||||
task.stat = STAT_CANCELLED if task.stat == STAT_WAITING else STAT_ENDED
|
task.stat = STAT_CANCELLED if task.stat == STAT_WAITING else STAT_ENDED
|
||||||
|
|
||||||
@ -241,14 +242,6 @@ class CalibreTask:
|
|||||||
# By default, we're good to clean a task if it's "Done"
|
# By default, we're good to clean a task if it's "Done"
|
||||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL, STAT_ENDED, STAT_CANCELLED)
|
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL, STAT_ENDED, STAT_CANCELLED)
|
||||||
|
|
||||||
'''@progress.setter
|
|
||||||
def progress(self, x):
|
|
||||||
if x > 1:
|
|
||||||
x = 1
|
|
||||||
if x < 0:
|
|
||||||
x = 0
|
|
||||||
self._progress = x'''
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def self_cleanup(self):
|
def self_cleanup(self):
|
||||||
return self._self_cleanup
|
return self._self_cleanup
|
||||||
|
@ -33,7 +33,7 @@ $(".datepicker").datepicker({
|
|||||||
if (results) {
|
if (results) {
|
||||||
pubDate = new Date(results[1], parseInt(results[2], 10) - 1, results[3]) || new Date(this.value);
|
pubDate = new Date(results[1], parseInt(results[2], 10) - 1, results[3]) || new Date(this.value);
|
||||||
$(this).next('input')
|
$(this).next('input')
|
||||||
.val(pubDate.toLocaleDateString(language))
|
.val(pubDate.toLocaleDateString(language.replaceAll("_","-")))
|
||||||
.removeClass("hidden");
|
.removeClass("hidden");
|
||||||
}
|
}
|
||||||
}).trigger("change");
|
}).trigger("change");
|
||||||
|
@ -92,14 +92,19 @@ $(function () {
|
|||||||
data: {"query": keyword},
|
data: {"query": keyword},
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
if (data.length) {
|
||||||
data.forEach(function(book) {
|
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
||||||
var $book = $(templates.bookResult(book));
|
data.forEach(function(book) {
|
||||||
$book.find("img").on("click", function () {
|
var $book = $(templates.bookResult(book));
|
||||||
populateForm(book);
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
$("#book-list").append($book);
|
||||||
});
|
});
|
||||||
$("#book-list").append($book);
|
}
|
||||||
});
|
else {
|
||||||
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "!</p>" + $("#meta-info")[0].innerHTML)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: function error() {
|
error: function error() {
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||||
|
@ -474,6 +474,17 @@ $(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$("#admin_refresh_cover_cache").click(function() {
|
||||||
|
confirmDialog("admin_refresh_cover_cache", "GeneralChangeModal", 0, function () {
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/ajax/updateThumbnails",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#restart_database").click(function() {
|
$("#restart_database").click(function() {
|
||||||
$("#DialogHeader").addClass("hidden");
|
$("#DialogHeader").addClass("hidden");
|
||||||
$("#DialogFinished").addClass("hidden");
|
$("#DialogFinished").addClass("hidden");
|
||||||
|
@ -550,7 +550,7 @@ $(function() {
|
|||||||
|
|
||||||
$("#user-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
$("#user-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||||
if (value === "denied_column_value") {
|
if (value === "denied_column_value") {
|
||||||
ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle);
|
confirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -641,9 +641,9 @@ function UserActions (value, row) {
|
|||||||
/* Function for cancelling tasks */
|
/* Function for cancelling tasks */
|
||||||
function TaskActions (value, row) {
|
function TaskActions (value, row) {
|
||||||
var cancellableStats = [0, 1, 2];
|
var cancellableStats = [0, 1, 2];
|
||||||
if (row.id && row.is_cancellable && cancellableStats.includes(row.stat)) {
|
if (row.task_id && row.is_cancellable && cancellableStats.includes(row.stat)) {
|
||||||
return [
|
return [
|
||||||
"<div class=\"task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.id + "\" title=\"Cancel\">",
|
"<div class=\"danger task-cancel\" data-toggle=\"modal\" data-target=\"#cancelTaskModal\" data-task-id=\"" + row.task_id + "\" title=\"Cancel\">",
|
||||||
"<i class=\"glyphicon glyphicon-ban-circle\"></i>",
|
"<i class=\"glyphicon glyphicon-ban-circle\"></i>",
|
||||||
"</div>"
|
"</div>"
|
||||||
].join("");
|
].join("");
|
||||||
|
@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
from cps import db
|
from cps import db
|
||||||
@ -41,10 +41,10 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
class TaskConvert(CalibreTask):
|
class TaskConvert(CalibreTask):
|
||||||
def __init__(self, file_path, bookid, taskMessage, settings, kindle_mail, user=None):
|
def __init__(self, file_path, book_id, task_message, settings, kindle_mail, user=None):
|
||||||
super(TaskConvert, self).__init__(taskMessage)
|
super(TaskConvert, self).__init__(task_message)
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.bookid = bookid
|
self.book_id = book_id
|
||||||
self.title = ""
|
self.title = ""
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.kindle_mail = kindle_mail
|
self.kindle_mail = kindle_mail
|
||||||
@ -56,9 +56,9 @@ class TaskConvert(CalibreTask):
|
|||||||
self.worker_thread = worker_thread
|
self.worker_thread = worker_thread
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
worker_db = db.CalibreDB(expire_on_commit=False)
|
worker_db = db.CalibreDB(expire_on_commit=False)
|
||||||
cur_book = worker_db.get_book(self.bookid)
|
cur_book = worker_db.get_book(self.book_id)
|
||||||
self.title = cur_book.title
|
self.title = cur_book.title
|
||||||
data = worker_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
data = worker_db.get_book_format(self.book_id, self.settings['old_book_format'])
|
||||||
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||||
data.name + "." + self.settings['old_book_format'].lower())
|
data.name + "." + self.settings['old_book_format'].lower())
|
||||||
if df:
|
if df:
|
||||||
@ -89,7 +89,7 @@ class TaskConvert(CalibreTask):
|
|||||||
# if we're sending to kindle after converting, create a one-off task and run it immediately
|
# if we're sending to kindle after converting, create a one-off task and run it immediately
|
||||||
# todo: figure out how to incorporate this into the progress
|
# todo: figure out how to incorporate this into the progress
|
||||||
try:
|
try:
|
||||||
EmailText = _(u"%(book)s send to Kindle", book=escape(self.title))
|
EmailText = N_(u"%(book)s send to Kindle", book=escape(self.title))
|
||||||
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
worker_thread.add(self.user, TaskEmail(self.settings['subject'],
|
||||||
self.results["path"],
|
self.results["path"],
|
||||||
filename,
|
filename,
|
||||||
@ -106,7 +106,7 @@ class TaskConvert(CalibreTask):
|
|||||||
error_message = None
|
error_message = None
|
||||||
local_db = db.CalibreDB(expire_on_commit=False)
|
local_db = db.CalibreDB(expire_on_commit=False)
|
||||||
file_path = self.file_path
|
file_path = self.file_path
|
||||||
book_id = self.bookid
|
book_id = self.book_id
|
||||||
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
||||||
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class TaskConvert(CalibreTask):
|
|||||||
# if it does - mark the conversion task as complete and return a success
|
# if it does - mark the conversion task as complete and return a success
|
||||||
# this will allow send to kindle workflow to continue to work
|
# this will allow send to kindle workflow to continue to work
|
||||||
if os.path.isfile(file_path + format_new_ext) or\
|
if os.path.isfile(file_path + format_new_ext) or\
|
||||||
local_db.get_book_format(self.bookid, self.settings['new_book_format']):
|
local_db.get_book_format(self.book_id, self.settings['new_book_format']):
|
||||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||||
cur_book = local_db.get_book(book_id)
|
cur_book = local_db.get_book(book_id)
|
||||||
self.title = cur_book.title
|
self.title = cur_book.title
|
||||||
@ -133,7 +133,7 @@ class TaskConvert(CalibreTask):
|
|||||||
local_db.session.rollback()
|
local_db.session.rollback()
|
||||||
log.error("Database error: %s", e)
|
log.error("Database error: %s", e)
|
||||||
local_db.session.close()
|
local_db.session.close()
|
||||||
self._handleError(error_message)
|
self._handleError(N_("Database error: %(error)s.", error=e))
|
||||||
return
|
return
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
local_db.session.close()
|
local_db.session.close()
|
||||||
@ -150,8 +150,7 @@ class TaskConvert(CalibreTask):
|
|||||||
else:
|
else:
|
||||||
# check if calibre converter-executable is existing
|
# check if calibre converter-executable is existing
|
||||||
if not os.path.exists(config.config_converterpath):
|
if not os.path.exists(config.config_converterpath):
|
||||||
# ToDo Text is not translated
|
self._handleError(N_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
||||||
self._handleError(_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
|
||||||
return
|
return
|
||||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
||||||
|
|
||||||
@ -184,11 +183,11 @@ class TaskConvert(CalibreTask):
|
|||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
return os.path.basename(file_path + format_new_ext)
|
return os.path.basename(file_path + format_new_ext)
|
||||||
else:
|
else:
|
||||||
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
|
error_message = N_('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||||
local_db.session.close()
|
local_db.session.close()
|
||||||
log.info("ebook converter failed with error while converting book")
|
log.info("ebook converter failed with error while converting book")
|
||||||
if not error_message:
|
if not error_message:
|
||||||
error_message = _('Ebook converter failed with unknown error')
|
error_message = N_('Ebook converter failed with unknown error')
|
||||||
self._handleError(error_message)
|
self._handleError(error_message)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -198,7 +197,7 @@ class TaskConvert(CalibreTask):
|
|||||||
try:
|
try:
|
||||||
p = process_open(command, quotes)
|
p = process_open(command, quotes)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return 1, _(u"Kepubify-converter failed: %(error)s", error=e)
|
return 1, N_(u"Kepubify-converter failed: %(error)s", error=e)
|
||||||
self.progress = 0.01
|
self.progress = 0.01
|
||||||
while True:
|
while True:
|
||||||
nextline = p.stdout.readlines()
|
nextline = p.stdout.readlines()
|
||||||
@ -219,7 +218,7 @@ class TaskConvert(CalibreTask):
|
|||||||
copyfile(converted_file[0], (file_path + format_new_ext))
|
copyfile(converted_file[0], (file_path + format_new_ext))
|
||||||
os.unlink(converted_file[0])
|
os.unlink(converted_file[0])
|
||||||
else:
|
else:
|
||||||
return 1, _(u"Converted file not found or more than one file in folder %(folder)s",
|
return 1, N_(u"Converted file not found or more than one file in folder %(folder)s",
|
||||||
folder=os.path.dirname(file_path))
|
folder=os.path.dirname(file_path))
|
||||||
return check, None
|
return check, None
|
||||||
|
|
||||||
@ -243,7 +242,7 @@ class TaskConvert(CalibreTask):
|
|||||||
|
|
||||||
p = process_open(command, quotes, newlines=False)
|
p = process_open(command, quotes, newlines=False)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
|
return 1, N_(u"Ebook-converter failed: %(error)s", error=e)
|
||||||
|
|
||||||
while p.poll() is None:
|
while p.poll() is None:
|
||||||
nextline = p.stdout.readline()
|
nextline = p.stdout.readline()
|
||||||
@ -266,15 +265,15 @@ class TaskConvert(CalibreTask):
|
|||||||
ele = ele.decode('utf-8', errors="ignore").strip('\n')
|
ele = ele.decode('utf-8', errors="ignore").strip('\n')
|
||||||
log.debug(ele)
|
log.debug(ele)
|
||||||
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
||||||
error_message = _("Calibre failed with error: %(error)s", error=ele)
|
error_message = N_("Calibre failed with error: %(error)s", error=ele)
|
||||||
return check, error_message
|
return check, error_message
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "Convert"
|
return N_("Convert")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Convert {} {}".format(self.bookid, self.kindle_mail)
|
return "Convert {} {}".format(self.book_id, self.kindle_mail)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
def is_cancellable(self):
|
||||||
|
@ -16,24 +16,22 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# 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 urllib.request import urlopen
|
||||||
|
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
from cps import config, logger
|
from cps import config, logger
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
|
|
||||||
try:
|
|
||||||
from urllib.request import urlopen
|
|
||||||
except ImportError as e:
|
|
||||||
from urllib2 import urlopen
|
|
||||||
|
|
||||||
|
|
||||||
class TaskReconnectDatabase(CalibreTask):
|
class TaskReconnectDatabase(CalibreTask):
|
||||||
def __init__(self, task_message=u'Reconnecting Calibre database'):
|
def __init__(self, task_message=N_('Reconnecting Calibre database')):
|
||||||
super(TaskReconnectDatabase, self).__init__(task_message)
|
super(TaskReconnectDatabase, self).__init__(task_message)
|
||||||
self.log = logger.create()
|
self.log = logger.create()
|
||||||
self.listen_address = config.get_config_ipaddress()
|
self.listen_address = config.get_config_ipaddress()
|
||||||
self.listen_port = config.config_port
|
self.listen_port = config.config_port
|
||||||
|
|
||||||
|
|
||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
address = self.listen_address if self.listen_address else 'localhost'
|
address = self.listen_address if self.listen_address else 'localhost'
|
||||||
port = self.listen_port if self.listen_port else 8083
|
port = self.listen_port if self.listen_port else 8083
|
||||||
@ -42,7 +40,7 @@ class TaskReconnectDatabase(CalibreTask):
|
|||||||
urlopen('http://' + address + ':' + str(port) + '/reconnect')
|
urlopen('http://' + address + ':' + str(port) + '/reconnect')
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._handleError(u'Unable to reconnect Calibre database: ' + str(ex))
|
self._handleError('Unable to reconnect Calibre database: ' + str(ex))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -26,9 +26,8 @@ from io import StringIO
|
|||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
|
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
from email import encoders
|
from email.utils import formatdate
|
||||||
from email.utils import formatdate, make_msgid
|
|
||||||
from email.generator import Generator
|
from email.generator import Generator
|
||||||
|
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
@ -111,13 +110,13 @@ class EmailSSL(EmailBase, smtplib.SMTP_SSL):
|
|||||||
|
|
||||||
|
|
||||||
class TaskEmail(CalibreTask):
|
class TaskEmail(CalibreTask):
|
||||||
def __init__(self, subject, filepath, attachment, settings, recipient, taskMessage, text, internal=False):
|
def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, internal=False):
|
||||||
super(TaskEmail, self).__init__(taskMessage)
|
super(TaskEmail, self).__init__(task_message)
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
self.recipent = recipient
|
self.recipient = recipient
|
||||||
self.text = text
|
self.text = text
|
||||||
self.asyncSMTP = None
|
self.asyncSMTP = None
|
||||||
self.results = dict()
|
self.results = dict()
|
||||||
@ -139,7 +138,7 @@ class TaskEmail(CalibreTask):
|
|||||||
message = EmailMessage()
|
message = EmailMessage()
|
||||||
# message = MIMEMultipart()
|
# message = MIMEMultipart()
|
||||||
message['From'] = self.settings["mail_from"]
|
message['From'] = self.settings["mail_from"]
|
||||||
message['To'] = self.recipent
|
message['To'] = self.recipient
|
||||||
message['Subject'] = self.subject
|
message['Subject'] = self.subject
|
||||||
message['Date'] = formatdate(localtime=True)
|
message['Date'] = formatdate(localtime=True)
|
||||||
message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain()) # f"<{uuid.uuid4()}@{get_msgid_domain(from_)}>" # make_msgid('calibre-web')
|
message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain()) # f"<{uuid.uuid4()}@{get_msgid_domain(from_)}>" # make_msgid('calibre-web')
|
||||||
@ -212,7 +211,7 @@ class TaskEmail(CalibreTask):
|
|||||||
gen = Generator(fp, mangle_from_=False)
|
gen = Generator(fp, mangle_from_=False)
|
||||||
gen.flatten(msg)
|
gen.flatten(msg)
|
||||||
|
|
||||||
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue())
|
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipient, fp.getvalue())
|
||||||
self.asyncSMTP.quit()
|
self.asyncSMTP.quit()
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
log.debug("E-mail send successfully")
|
log.debug("E-mail send successfully")
|
||||||
@ -264,7 +263,7 @@ class TaskEmail(CalibreTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "E-mail"
|
return N_("E-mail")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
def is_cancellable(self):
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# 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
|
|
||||||
import os
|
import os
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ from cps import config, db, fs, gdriveutils, logger, ub
|
|||||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import func, text, or_
|
from sqlalchemy import func, text, or_
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
@ -50,7 +49,7 @@ def get_best_fit(width, height, image_width, image_height):
|
|||||||
resize_height = int(height / 2.0)
|
resize_height = int(height / 2.0)
|
||||||
aspect_ratio = image_width / image_height
|
aspect_ratio = image_width / image_height
|
||||||
|
|
||||||
# If this image's aspect ratio is different than the first image, then resize this image
|
# If this image's aspect ratio is different from the first image, then resize this image
|
||||||
# to fill the width and height of the first image
|
# to fill the width and height of the first image
|
||||||
if aspect_ratio < width / height:
|
if aspect_ratio < width / height:
|
||||||
resize_width = int(width / 2.0)
|
resize_width = int(width / 2.0)
|
||||||
@ -64,9 +63,10 @@ def get_best_fit(width, height, image_width, image_height):
|
|||||||
|
|
||||||
|
|
||||||
class TaskGenerateCoverThumbnails(CalibreTask):
|
class TaskGenerateCoverThumbnails(CalibreTask):
|
||||||
def __init__(self, task_message=''):
|
def __init__(self, book_id=-1, task_message=''):
|
||||||
super(TaskGenerateCoverThumbnails, self).__init__(task_message)
|
super(TaskGenerateCoverThumbnails, self).__init__(task_message)
|
||||||
self.log = logger.create()
|
self.log = logger.create()
|
||||||
|
self.book_id = book_id
|
||||||
self.app_db_session = ub.get_new_session_instance()
|
self.app_db_session = ub.get_new_session_instance()
|
||||||
self.calibre_db = db.CalibreDB(expire_on_commit=False)
|
self.calibre_db = db.CalibreDB(expire_on_commit=False)
|
||||||
self.cache = fs.FileSystem()
|
self.cache = fs.FileSystem()
|
||||||
@ -78,37 +78,21 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
if self.calibre_db.session and use_IM and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
|
if self.calibre_db.session and use_IM and self.stat != STAT_CANCELLED and self.stat != STAT_ENDED:
|
||||||
self.message = 'Scanning Books'
|
self.message = 'Scanning Books'
|
||||||
books_with_covers = self.get_books_with_covers()
|
books_with_covers = self.get_books_with_covers(self.book_id)
|
||||||
count = len(books_with_covers)
|
count = len(books_with_covers)
|
||||||
|
|
||||||
total_generated = 0
|
total_generated = 0
|
||||||
for i, book in enumerate(books_with_covers):
|
for i, book in enumerate(books_with_covers):
|
||||||
generated = 0
|
|
||||||
book_cover_thumbnails = self.get_book_cover_thumbnails(book.id)
|
|
||||||
|
|
||||||
# Generate new thumbnails for missing covers
|
# Generate new thumbnails for missing covers
|
||||||
resolutions = list(map(lambda t: t.resolution, book_cover_thumbnails))
|
generated = self.create_book_cover_thumbnails(book)
|
||||||
missing_resolutions = list(set(self.resolutions).difference(resolutions))
|
|
||||||
for resolution in missing_resolutions:
|
|
||||||
generated += 1
|
|
||||||
self.create_book_cover_thumbnail(book, resolution)
|
|
||||||
|
|
||||||
# Replace outdated or missing thumbnails
|
|
||||||
for thumbnail in book_cover_thumbnails:
|
|
||||||
if book.last_modified > thumbnail.generated_at:
|
|
||||||
generated += 1
|
|
||||||
self.update_book_cover_thumbnail(book, thumbnail)
|
|
||||||
|
|
||||||
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
|
|
||||||
generated += 1
|
|
||||||
self.update_book_cover_thumbnail(book, thumbnail)
|
|
||||||
|
|
||||||
# Increment the progress
|
# Increment the progress
|
||||||
self.progress = (1.0 / count) * i
|
self.progress = (1.0 / count) * i
|
||||||
|
|
||||||
if generated > 0:
|
if generated > 0:
|
||||||
total_generated += generated
|
total_generated += generated
|
||||||
self.message = u'Generated {0} cover thumbnails'.format(total_generated)
|
self.message = N_(u'Generated %(count)s cover thumbnails', count=total_generated)
|
||||||
|
|
||||||
# Check if job has been cancelled or ended
|
# Check if job has been cancelled or ended
|
||||||
if self.stat == STAT_CANCELLED:
|
if self.stat == STAT_CANCELLED:
|
||||||
@ -125,10 +109,12 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
self.app_db_session.remove()
|
self.app_db_session.remove()
|
||||||
|
|
||||||
def get_books_with_covers(self):
|
def get_books_with_covers(self, book_id=-1):
|
||||||
|
filter_exp = (db.Books.id == book_id) if book_id != -1 else True
|
||||||
return self.calibre_db.session \
|
return self.calibre_db.session \
|
||||||
.query(db.Books) \
|
.query(db.Books) \
|
||||||
.filter(db.Books.has_cover == 1) \
|
.filter(db.Books.has_cover == 1) \
|
||||||
|
.filter(filter_exp) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def get_book_cover_thumbnails(self, book_id):
|
def get_book_cover_thumbnails(self, book_id):
|
||||||
@ -139,7 +125,29 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
def create_book_cover_thumbnail(self, book, resolution):
|
def create_book_cover_thumbnails(self, book):
|
||||||
|
generated = 0
|
||||||
|
book_cover_thumbnails = self.get_book_cover_thumbnails(book.id)
|
||||||
|
|
||||||
|
# Generate new thumbnails for missing covers
|
||||||
|
resolutions = list(map(lambda t: t.resolution, book_cover_thumbnails))
|
||||||
|
missing_resolutions = list(set(self.resolutions).difference(resolutions))
|
||||||
|
for resolution in missing_resolutions:
|
||||||
|
generated += 1
|
||||||
|
self.create_book_cover_single_thumbnail(book, resolution)
|
||||||
|
|
||||||
|
# Replace outdated or missing thumbnails
|
||||||
|
for thumbnail in book_cover_thumbnails:
|
||||||
|
if book.last_modified > thumbnail.generated_at:
|
||||||
|
generated += 1
|
||||||
|
self.update_book_cover_thumbnail(book, thumbnail)
|
||||||
|
|
||||||
|
elif not self.cache.get_cache_file_exists(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS):
|
||||||
|
generated += 1
|
||||||
|
self.update_book_cover_thumbnail(book, thumbnail)
|
||||||
|
return generated
|
||||||
|
|
||||||
|
def create_book_cover_single_thumbnail(self, book, resolution):
|
||||||
thumbnail = ub.Thumbnail()
|
thumbnail = ub.Thumbnail()
|
||||||
thumbnail.type = constants.THUMBNAIL_TYPE_COVER
|
thumbnail.type = constants.THUMBNAIL_TYPE_COVER
|
||||||
thumbnail.entity_id = book.id
|
thumbnail.entity_id = book.id
|
||||||
@ -151,8 +159,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
self.generate_book_thumbnail(book, thumbnail)
|
self.generate_book_thumbnail(book, thumbnail)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error creating book thumbnail: ' + str(ex))
|
self.log.info('Error creating book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error creating book thumbnail: ' + str(ex))
|
self._handleError('Error creating book thumbnail: ' + str(ex))
|
||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def update_book_cover_thumbnail(self, book, thumbnail):
|
def update_book_cover_thumbnail(self, book, thumbnail):
|
||||||
@ -163,8 +171,8 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
||||||
self.generate_book_thumbnail(book, thumbnail)
|
self.generate_book_thumbnail(book, thumbnail)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error updating book thumbnail: ' + str(ex))
|
self.log.info('Error updating book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error updating book thumbnail: ' + str(ex))
|
self._handleError('Error updating book thumbnail: ' + str(ex))
|
||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def generate_book_thumbnail(self, book, thumbnail):
|
def generate_book_thumbnail(self, book, thumbnail):
|
||||||
@ -191,7 +199,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
img.save(filename=filename)
|
img.save(filename=filename)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
# Bubble exception to calling function
|
# Bubble exception to calling function
|
||||||
self.log.info(u'Error generating thumbnail file: ' + str(ex))
|
self.log.info('Error generating thumbnail file: ' + str(ex))
|
||||||
raise ex
|
raise ex
|
||||||
finally:
|
finally:
|
||||||
if stream is not None:
|
if stream is not None:
|
||||||
@ -212,10 +220,13 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return 'GenerateCoverThumbnails'
|
return N_('Cover Thumbnails')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "GenerateCoverThumbnails"
|
if self.book_id > 0:
|
||||||
|
return "Add Cover Thumbnails for Book {}".format(self.book_id)
|
||||||
|
else:
|
||||||
|
return "Generate Cover Thumbnails"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancellable(self):
|
def is_cancellable(self):
|
||||||
@ -268,7 +279,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
|
|
||||||
if generated > 0:
|
if generated > 0:
|
||||||
total_generated += generated
|
total_generated += generated
|
||||||
self.message = u'Generated {0} series thumbnails'.format(total_generated)
|
self.message = N_('Generated {0} series thumbnails').format(total_generated)
|
||||||
|
|
||||||
# Check if job has been cancelled or ended
|
# Check if job has been cancelled or ended
|
||||||
if self.stat == STAT_CANCELLED:
|
if self.stat == STAT_CANCELLED:
|
||||||
@ -324,8 +335,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
self.generate_series_thumbnail(series_books, thumbnail)
|
self.generate_series_thumbnail(series_books, thumbnail)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error creating book thumbnail: ' + str(ex))
|
self.log.info('Error creating book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error creating book thumbnail: ' + str(ex))
|
self._handleError('Error creating book thumbnail: ' + str(ex))
|
||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def update_series_thumbnail(self, series_books, thumbnail):
|
def update_series_thumbnail(self, series_books, thumbnail):
|
||||||
@ -336,8 +347,8 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
||||||
self.generate_series_thumbnail(series_books, thumbnail)
|
self.generate_series_thumbnail(series_books, thumbnail)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error updating book thumbnail: ' + str(ex))
|
self.log.info('Error updating book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error updating book thumbnail: ' + str(ex))
|
self._handleError('Error updating book thumbnail: ' + str(ex))
|
||||||
self.app_db_session.rollback()
|
self.app_db_session.rollback()
|
||||||
|
|
||||||
def generate_series_thumbnail(self, series_books, thumbnail):
|
def generate_series_thumbnail(self, series_books, thumbnail):
|
||||||
@ -380,7 +391,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
canvas.composite(img, left, top)
|
canvas.composite(img, left, top)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error generating thumbnail file: ' + str(ex))
|
self.log.info('Error generating thumbnail file: ' + str(ex))
|
||||||
raise ex
|
raise ex
|
||||||
finally:
|
finally:
|
||||||
if stream is not None:
|
if stream is not None:
|
||||||
@ -422,7 +433,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return 'GenerateSeriesThumbnails'
|
return N_('Cover Thumbnails')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "GenerateSeriesThumbnails"
|
return "GenerateSeriesThumbnails"
|
||||||
@ -433,22 +444,28 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
|||||||
|
|
||||||
|
|
||||||
class TaskClearCoverThumbnailCache(CalibreTask):
|
class TaskClearCoverThumbnailCache(CalibreTask):
|
||||||
def __init__(self, book_id, task_message=u'Clearing cover thumbnail cache'):
|
def __init__(self, book_id, task_message=N_('Clearing cover thumbnail cache')):
|
||||||
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
|
super(TaskClearCoverThumbnailCache, self).__init__(task_message)
|
||||||
self.log = logger.create()
|
self.log = logger.create()
|
||||||
self.book_id = book_id
|
self.book_id = book_id
|
||||||
|
self.calibre_db = db.CalibreDB(expire_on_commit=False)
|
||||||
self.app_db_session = ub.get_new_session_instance()
|
self.app_db_session = ub.get_new_session_instance()
|
||||||
self.cache = fs.FileSystem()
|
self.cache = fs.FileSystem()
|
||||||
|
|
||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
if self.app_db_session:
|
if self.app_db_session:
|
||||||
if self.book_id > 0: # make sure all thumbnails aren't getting deleted due to a bug
|
if self.book_id == 0: # delete superfluous thumbnails
|
||||||
|
thumbnails = (self.calibre_db.session.query(ub.Thumbnail)
|
||||||
|
.join(db.Books, ub.Thumbnail.entity_id == db.Books.id, isouter=True)
|
||||||
|
.filter(db.Books.id == None)
|
||||||
|
.all())
|
||||||
|
elif self.book_id > 0: # make sure single book is selected
|
||||||
thumbnails = self.get_thumbnails_for_book(self.book_id)
|
thumbnails = self.get_thumbnails_for_book(self.book_id)
|
||||||
|
if self.book_id < 0:
|
||||||
|
self.delete_all_thumbnails()
|
||||||
|
else:
|
||||||
for thumbnail in thumbnails:
|
for thumbnail in thumbnails:
|
||||||
self.delete_thumbnail(thumbnail)
|
self.delete_thumbnail(thumbnail)
|
||||||
else:
|
|
||||||
self.delete_all_thumbnails()
|
|
||||||
|
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
self.app_db_session.remove()
|
self.app_db_session.remove()
|
||||||
|
|
||||||
@ -460,7 +477,6 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
def delete_thumbnail(self, thumbnail):
|
def delete_thumbnail(self, thumbnail):
|
||||||
# thumbnail.expiration = datetime.utcnow()
|
|
||||||
try:
|
try:
|
||||||
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
self.cache.delete_cache_file(thumbnail.filename, constants.CACHE_TYPE_THUMBNAILS)
|
||||||
self.app_db_session \
|
self.app_db_session \
|
||||||
@ -470,8 +486,8 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
|||||||
.delete()
|
.delete()
|
||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error deleting book thumbnail: ' + str(ex))
|
self.log.info('Error deleting book thumbnail: ' + str(ex))
|
||||||
self._handleError(u'Error deleting book thumbnail: ' + str(ex))
|
self._handleError('Error deleting book thumbnail: ' + str(ex))
|
||||||
|
|
||||||
def delete_all_thumbnails(self):
|
def delete_all_thumbnails(self):
|
||||||
try:
|
try:
|
||||||
@ -479,16 +495,17 @@ class TaskClearCoverThumbnailCache(CalibreTask):
|
|||||||
self.app_db_session.commit()
|
self.app_db_session.commit()
|
||||||
self.cache.delete_cache_dir(constants.CACHE_TYPE_THUMBNAILS)
|
self.cache.delete_cache_dir(constants.CACHE_TYPE_THUMBNAILS)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info(u'Error deleting thumbnail directory: ' + str(ex))
|
self.log.info('Error deleting thumbnail directory: ' + str(ex))
|
||||||
self._handleError(u'Error deleting thumbnail directory: ' + str(ex))
|
self._handleError('Error deleting thumbnail directory: ' + str(ex))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return 'ThumbnailsClear'
|
return N_('Cover Thumbnails')
|
||||||
|
|
||||||
|
# needed for logging
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.book_id > 0:
|
if self.book_id > 0:
|
||||||
return "Delete Thumbnail cache for book " + str(self.book_id)
|
return "Replace/Delete Cover Thumbnails for book " + str(self.book_id)
|
||||||
else:
|
else:
|
||||||
return "Delete Thumbnail cache directory"
|
return "Delete Thumbnail cache directory"
|
||||||
|
|
||||||
|
@ -17,11 +17,14 @@
|
|||||||
# 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 datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask_babel import lazy_gettext as N_
|
||||||
|
|
||||||
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
|
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
|
||||||
|
|
||||||
class TaskUpload(CalibreTask):
|
class TaskUpload(CalibreTask):
|
||||||
def __init__(self, taskMessage, book_title):
|
def __init__(self, task_message, book_title):
|
||||||
super(TaskUpload, self).__init__(taskMessage)
|
super(TaskUpload, self).__init__(task_message)
|
||||||
self.start_time = self.end_time = datetime.now()
|
self.start_time = self.end_time = datetime.now()
|
||||||
self.stat = STAT_FINISH_SUCCESS
|
self.stat = STAT_FINISH_SUCCESS
|
||||||
self.progress = 1
|
self.progress = 1
|
||||||
@ -32,7 +35,7 @@ class TaskUpload(CalibreTask):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return "Upload"
|
return N_("Upload")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Upload {}".format(self.book_title)
|
return "Upload {}".format(self.book_title)
|
||||||
|
@ -161,32 +161,40 @@
|
|||||||
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
|
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if feature_support['scheduler'] %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>{{_('Scheduled Tasks')}}</h2>
|
<h2>{{_('Scheduled Tasks')}}</h2>
|
||||||
<div class="col-xs-12 col-sm-12 scheduled_tasks_details">
|
<div class="col-xs-12 col-sm-12 scheduled_tasks_details">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks start to run')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks start to run')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{config.schedule_start_time}}:00</div>
|
<div class="col-xs-6 col-sm-3">{{schedule_time}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Time at which tasks stop running')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Maximum tasks duration')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{config.schedule_end_time}}:00</div>
|
<div class="col-xs-6 col-sm-3">{{schedule_duration}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Generate book cover thumbnails')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Generate book cover thumbnails')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_book_covers) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_book_covers) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<!--div class="row">
|
||||||
<div class="col-xs-6 col-sm-3">{{_('Generate series cover thumbnails')}}</div>
|
<div class="col-xs-6 col-sm-3">{{_('Generate series cover thumbnails')}}</div>
|
||||||
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_series_covers) }}</div>
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_generate_series_covers) }}</div>
|
||||||
|
</div-->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6 col-sm-3">{{_('Reconnect to Calibre Library')}}</div>
|
||||||
|
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.schedule_reconnect) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-default scheduledtasks" id="admin_edit_scheduled_tasks" href="{{url_for('admin.edit_scheduledtasks')}}">{{_('Edit Scheduled Tasks Settings')}}</a>
|
<a class="btn btn-default scheduledtasks" id="admin_edit_scheduled_tasks" href="{{url_for('admin.edit_scheduledtasks')}}">{{_('Edit Scheduled Tasks Settings')}}</a>
|
||||||
|
{% if config.schedule_generate_book_covers %}
|
||||||
|
<a class="btn btn-default" id="admin_refresh_cover_cache">{{_('Refresh Thumbnail Cover Cache')}}</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<h2>{{_('Administration')}}</h2>
|
<h2>{{_('Administration')}}</h2>
|
||||||
<a class="btn btn-default" id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a>
|
<a class="btn btn-default" id="debug" href="{{url_for('admin.download_debug')}}">{{_('Download Debug Package')}}</a>
|
||||||
@ -279,3 +287,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ change_confirm_modal() }}
|
||||||
|
{% endblock %}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
{% extends "layout.html" %}
|
|
||||||
{% block body %}
|
|
||||||
<h1>{{title}}</h1>
|
|
||||||
<div class="filterheader hidden-xs">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<div id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary {% if order == 1 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet"></span></div>
|
|
||||||
<div id="desc" data-id="{{ data }}" class="btn btn-primary{% if order == 0 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></div>
|
|
||||||
{% if charlist|length %}
|
|
||||||
<div id="all" class="active btn btn-primary {% if charlist|length > 9 %}hidden-sm{% endif %}">{{_('All')}}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
|
||||||
{% for char in charlist%}
|
|
||||||
<div class="btn btn-primary char">{{char}}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div div id="list" class="col-xs-12 col-sm-6">
|
|
||||||
{% for lang in languages %}
|
|
||||||
{% if loop.index0 == (loop.length/2)|int and loop.length > 20 %}
|
|
||||||
</div>
|
|
||||||
<div id="second" class="col-xs-12 col-sm-6">
|
|
||||||
{% endif %}
|
|
||||||
<div class="row" data-id="{{lang[0].name}}">
|
|
||||||
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang[1]}}</span></div>
|
|
||||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang[0].lang_code, data=data, sort_param='stored')}}">{{lang[0].name}}</a></div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
{% block js %}
|
|
||||||
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
||||||
{% for char in charlist%}
|
{% for char in charlist%}
|
||||||
<div class="btn btn-primary char">{{char.char}}</div>
|
<div class="btn btn-primary char">{{char[0]}}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -29,8 +29,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="second" class="col-xs-12 col-sm-6">
|
<div id="second" class="col-xs-12 col-sm-6">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}">
|
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry[0].format %}{{entry[0].format}}{% else %}{% if entry[0].rating %}{{entry[0].rating}}{% else %}{{entry[0].name}}{% endif %}{% endif %}{% endif %}">
|
||||||
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div>
|
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry[1]}}</span></div>
|
||||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].id )}}{% endif %}">
|
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].id )}}{% endif %}">
|
||||||
{% if entry.name %}
|
{% if entry.name %}
|
||||||
<div class="rating">
|
<div class="rating">
|
||||||
|
@ -11,16 +11,16 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="schedule_start_time">{{_('Time at which tasks start to run')}}</label>
|
<label for="schedule_start_time">{{_('Time at which tasks start to run')}}</label>
|
||||||
<select name="schedule_start_time" id="schedule_start_time" class="form-control">
|
<select name="schedule_start_time" id="schedule_start_time" class="form-control">
|
||||||
{% for n in range(24) %}
|
{% for n in starttime %}
|
||||||
<option value="{{n}}" {% if config.schedule_start_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
|
<option value="{{n[0]}}" {% if config.schedule_start_time == n[0] %}selected{% endif %}>{{n[1]}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="schedule_end_time">{{_('Time at which tasks stop running')}}</label>
|
<label for="schedule_duration">{{_('Maximum tasks duration')}}</label>
|
||||||
<select name="schedule_end_time" id="schedule_end_time" class="form-control">
|
<select name="schedule_duration" id="schedule_duration" class="form-control">
|
||||||
{% for n in range(24) %}
|
{% for n in duration %}
|
||||||
<option value="{{n}}" {% if config.schedule_end_time == n %}selected{% endif %}>{{n}}{{_(':00')}}</option>
|
<option value="{{n[0]}}" {% if config.schedule_duration == n[0] %}selected{% endif %}>{{n[1]}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -28,10 +28,15 @@
|
|||||||
<input type="checkbox" id="schedule_generate_book_covers" name="schedule_generate_book_covers" {% if config.schedule_generate_book_covers %}checked{% endif %}>
|
<input type="checkbox" id="schedule_generate_book_covers" name="schedule_generate_book_covers" {% if config.schedule_generate_book_covers %}checked{% endif %}>
|
||||||
<label for="schedule_generate_book_covers">{{_('Generate Book Cover Thumbnails')}}</label>
|
<label for="schedule_generate_book_covers">{{_('Generate Book Cover Thumbnails')}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<!--div class="form-group">
|
||||||
<input type="checkbox" id="schedule_generate_series_covers" name="schedule_generate_series_covers" {% if config.schedule_generate_series_covers %}checked{% endif %}>
|
<input type="checkbox" id="schedule_generate_series_covers" name="schedule_generate_series_covers" {% if config.schedule_generate_series_covers %}checked{% endif %}>
|
||||||
<label for="schedule_generate_series_covers">{{_('Generate Series Cover Thumbnails')}}</label>
|
<label for="schedule_generate_series_covers">{{_('Generate Series Cover Thumbnails')}}</label>
|
||||||
|
</div-->
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="schedule_reconnect" name="schedule_reconnect" {% if config.schedule_generate_book_covers %}checked{% endif %}>
|
||||||
|
<label for="schedule_reconnect">{{_('Reconnect to Calibre Library')}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
|
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||||
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Cancel')}}</a>
|
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Cancel')}}</a>
|
||||||
</form>
|
</form>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
{% if version %}
|
{% if version %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{library}}</th>
|
<th>{{library}}</th>
|
||||||
<td>{{_(version)}}</td>
|
<td>{{version}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -107,52 +107,10 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="",
|
languages="",
|
||||||
publisher="")
|
publisher="",
|
||||||
|
pubdate="",
|
||||||
|
identifiers=[]
|
||||||
def parse_xmp(pdf_file):
|
)
|
||||||
"""
|
|
||||||
Parse XMP Metadata and prepare for BookMeta object
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
xmp_info = pdf_file.getXmpMetadata()
|
|
||||||
except Exception as ex:
|
|
||||||
log.debug('Can not read XMP metadata {}'.format(ex))
|
|
||||||
return None
|
|
||||||
|
|
||||||
if xmp_info:
|
|
||||||
try:
|
|
||||||
xmp_author = xmp_info.dc_creator # list
|
|
||||||
except AttributeError:
|
|
||||||
xmp_author = ['']
|
|
||||||
|
|
||||||
if xmp_info.dc_title:
|
|
||||||
xmp_title = xmp_info.dc_title['x-default']
|
|
||||||
else:
|
|
||||||
xmp_title = ''
|
|
||||||
|
|
||||||
if xmp_info.dc_description:
|
|
||||||
xmp_description = xmp_info.dc_description['x-default']
|
|
||||||
else:
|
|
||||||
xmp_description = ''
|
|
||||||
|
|
||||||
languages = []
|
|
||||||
try:
|
|
||||||
for i in xmp_info.dc_language:
|
|
||||||
#calibre-web currently only takes one language.
|
|
||||||
languages.append(isoLanguages.get_lang3(i))
|
|
||||||
except AttributeError:
|
|
||||||
languages.append('')
|
|
||||||
|
|
||||||
xmp_tags = ', '.join(xmp_info.dc_subject)
|
|
||||||
xmp_publisher = ', '.join(xmp_info.dc_publisher)
|
|
||||||
|
|
||||||
return {'author': xmp_author,
|
|
||||||
'title': xmp_title,
|
|
||||||
'subject': xmp_description,
|
|
||||||
'tags': xmp_tags, 'languages': languages,
|
|
||||||
'publisher': xmp_publisher
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xmp(pdf_file):
|
def parse_xmp(pdf_file):
|
||||||
@ -251,7 +209,9 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages=','.join(languages),
|
languages=','.join(languages),
|
||||||
publisher=publisher)
|
publisher=publisher,
|
||||||
|
pubdate="",
|
||||||
|
identifiers=[])
|
||||||
|
|
||||||
|
|
||||||
def pdf_preview(tmp_file_path, tmp_dir):
|
def pdf_preview(tmp_file_path, tmp_dir):
|
||||||
|
230
cps/web.py
230
cps/web.py
@ -307,10 +307,20 @@ def get_matching_tags():
|
|||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
|
|
||||||
def generate_char_list(data_colum, db_link):
|
def generate_char_list(entries): # data_colum, db_link):
|
||||||
return (calibre_db.session.query(func.upper(func.substr(data_colum, 1, 1)).label('char'))
|
char_list = list()
|
||||||
|
for entry in entries:
|
||||||
|
upper_char = entry[0].name[0].upper()
|
||||||
|
if upper_char not in char_list:
|
||||||
|
char_list.append(upper_char)
|
||||||
|
return char_list
|
||||||
|
|
||||||
|
|
||||||
|
def query_char_list(data_colum, db_link):
|
||||||
|
results = (calibre_db.session.query(func.upper(func.substr(data_colum, 1, 1)).label('char'))
|
||||||
.join(db_link).join(db.Books).filter(calibre_db.common_filters())
|
.join(db_link).join(db.Books).filter(calibre_db.common_filters())
|
||||||
.group_by(func.upper(func.substr(data_colum, 1, 1))).all())
|
.group_by(func.upper(func.substr(data_colum, 1, 1))).all())
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_sort_function(sort_param, data):
|
def get_sort_function(sort_param, data):
|
||||||
@ -526,50 +536,92 @@ def render_author_books(page, author_id, order):
|
|||||||
|
|
||||||
|
|
||||||
def render_publisher_books(page, book_id, order):
|
def render_publisher_books(page, book_id, order):
|
||||||
publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
|
if book_id == '-1':
|
||||||
if publisher:
|
|
||||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
db.Books,
|
db.Books,
|
||||||
db.Books.publishers.any(db.Publishers.id == book_id),
|
db.Publishers.name == None,
|
||||||
[db.Series.name, order[0][0], db.Books.series_index],
|
[db.Series.name, order[0][0], db.Books.series_index],
|
||||||
True, config.config_read_column,
|
True, config.config_read_column,
|
||||||
|
db.books_publishers_link,
|
||||||
|
db.Books.id == db.books_publishers_link.c.book,
|
||||||
|
db.Publishers,
|
||||||
|
db.books_series_link,
|
||||||
|
db.Books.id == db.books_series_link.c.book,
|
||||||
|
db.Series)
|
||||||
|
publisher = _("None")
|
||||||
|
else:
|
||||||
|
publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
|
||||||
|
if publisher:
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
|
db.Books,
|
||||||
|
db.Books.publishers.any(
|
||||||
|
db.Publishers.id == book_id),
|
||||||
|
[db.Series.name, order[0][0],
|
||||||
|
db.Books.series_index],
|
||||||
|
True, config.config_read_column,
|
||||||
|
db.books_series_link,
|
||||||
|
db.Books.id == db.books_series_link.c.book,
|
||||||
|
db.Series)
|
||||||
|
publisher = publisher.name
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
|
||||||
|
title=_(u"Publisher: %(name)s", name=publisher),
|
||||||
|
page="publisher",
|
||||||
|
order=order[1])
|
||||||
|
|
||||||
|
|
||||||
|
def render_series_books(page, book_id, order):
|
||||||
|
if book_id == '-1':
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
|
db.Books,
|
||||||
|
db.Series.name == None,
|
||||||
|
[order[0][0]],
|
||||||
|
True, config.config_read_column,
|
||||||
db.books_series_link,
|
db.books_series_link,
|
||||||
db.Books.id == db.books_series_link.c.book,
|
db.Books.id == db.books_series_link.c.book,
|
||||||
db.Series)
|
db.Series)
|
||||||
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
|
series_name = _("None")
|
||||||
title=_(u"Publisher: %(name)s", name=publisher.name),
|
|
||||||
page="publisher",
|
|
||||||
order=order[1])
|
|
||||||
else:
|
else:
|
||||||
abort(404)
|
series_name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
|
||||||
|
if series_name:
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
def render_series_books(page, book_id, order):
|
db.Books,
|
||||||
name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
|
db.Books.series.any(db.Series.id == book_id),
|
||||||
if name:
|
[order[0][0]],
|
||||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
True, config.config_read_column)
|
||||||
db.Books,
|
series_name = series_name.name
|
||||||
db.Books.series.any(db.Series.id == book_id),
|
else:
|
||||||
[order[0][0]],
|
abort(404)
|
||||||
True, config.config_read_column)
|
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=series_name), page="series", order=order[1])
|
||||||
title=_(u"Series: %(serie)s", serie=name.name), page="series", order=order[1])
|
|
||||||
else:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
|
|
||||||
def render_ratings_books(page, book_id, order):
|
def render_ratings_books(page, book_id, order):
|
||||||
name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
|
if book_id == '-1':
|
||||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
db.Books,
|
db.Books,
|
||||||
db.Books.ratings.any(db.Ratings.id == book_id),
|
db.Books.ratings == None,
|
||||||
[order[0][0]],
|
[order[0][0]],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column,
|
||||||
if name and name.rating <= 10:
|
db.books_series_link,
|
||||||
|
db.Books.id == db.books_series_link.c.book,
|
||||||
|
db.Series)
|
||||||
|
title = _(u"Rating: None")
|
||||||
|
rating = -1
|
||||||
|
else:
|
||||||
|
name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
|
db.Books,
|
||||||
|
db.Books.ratings.any(db.Ratings.id == book_id),
|
||||||
|
[order[0][0]],
|
||||||
|
True, config.config_read_column)
|
||||||
|
title = _(u"Rating: %(rating)s stars", rating=int(name.rating / 2))
|
||||||
|
rating = name.rating
|
||||||
|
if title and rating <= 10:
|
||||||
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)),
|
title=title, page="ratings", order=order[1])
|
||||||
page="ratings",
|
|
||||||
order=order[1])
|
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@ -591,33 +643,61 @@ def render_formats_books(page, book_id, order):
|
|||||||
|
|
||||||
|
|
||||||
def render_category_books(page, book_id, order):
|
def render_category_books(page, book_id, order):
|
||||||
name = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
|
if book_id == '-1':
|
||||||
if name:
|
|
||||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
db.Books,
|
db.Books,
|
||||||
db.Books.tags.any(db.Tags.id == book_id),
|
db.Tags.name == None,
|
||||||
[order[0][0], db.Series.name, db.Books.series_index],
|
[order[0][0], db.Series.name, db.Books.series_index],
|
||||||
True, config.config_read_column,
|
True, config.config_read_column,
|
||||||
|
db.books_tags_link,
|
||||||
|
db.Books.id == db.books_tags_link.c.book,
|
||||||
|
db.Tags,
|
||||||
db.books_series_link,
|
db.books_series_link,
|
||||||
db.Books.id == db.books_series_link.c.book,
|
db.Books.id == db.books_series_link.c.book,
|
||||||
db.Series)
|
db.Series)
|
||||||
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
|
tagsname = _("None")
|
||||||
title=_(u"Category: %(name)s", name=name.name), page="category", order=order[1])
|
|
||||||
else:
|
else:
|
||||||
abort(404)
|
tagsname = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
|
||||||
|
if tagsname:
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
|
db.Books,
|
||||||
|
db.Books.tags.any(db.Tags.id == book_id),
|
||||||
|
[order[0][0], db.Series.name,
|
||||||
|
db.Books.series_index],
|
||||||
|
True, config.config_read_column,
|
||||||
|
db.books_series_link,
|
||||||
|
db.Books.id == db.books_series_link.c.book,
|
||||||
|
db.Series)
|
||||||
|
tagsname = tagsname.name
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
|
||||||
|
title=_(u"Category: %(name)s", name=tagsname), page="category", order=order[1])
|
||||||
|
|
||||||
|
|
||||||
def render_language_books(page, name, order):
|
def render_language_books(page, name, order):
|
||||||
try:
|
try:
|
||||||
lang_name = isoLanguages.get_language_name(get_locale(), name)
|
if name.lower() != "none":
|
||||||
|
lang_name = isoLanguages.get_language_name(get_locale(), name)
|
||||||
|
else:
|
||||||
|
lang_name = _("None")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
if name == "none":
|
||||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
db.Books,
|
db.Books,
|
||||||
db.Books.languages.any(db.Languages.lang_code == name),
|
db.Languages.lang_code == None,
|
||||||
[order[0][0]],
|
[order[0][0]],
|
||||||
True, config.config_read_column)
|
True, config.config_read_column,
|
||||||
|
db.books_languages_link,
|
||||||
|
db.Books.id == db.books_languages_link.c.book,
|
||||||
|
db.Languages)
|
||||||
|
else:
|
||||||
|
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||||
|
db.Books,
|
||||||
|
db.Books.languages.any(db.Languages.lang_code == name),
|
||||||
|
[order[0][0]],
|
||||||
|
True, config.config_read_column)
|
||||||
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", order=order[1])
|
title=_(u"Language: %(name)s", name=lang_name), page="language", order=order[1])
|
||||||
|
|
||||||
@ -880,7 +960,7 @@ def author_list():
|
|||||||
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
|
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
|
||||||
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
|
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
|
||||||
.group_by(text('books_authors_link.author')).order_by(order).all()
|
.group_by(text('books_authors_link.author')).order_by(order).all()
|
||||||
char_list = generate_char_list(db.Authors.sort, db.books_authors_link)
|
char_list = query_char_list(db.Authors.sort, db.books_authors_link)
|
||||||
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
|
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
|
||||||
# starts a change session
|
# starts a change session
|
||||||
author_copy = copy.deepcopy(entries)
|
author_copy = copy.deepcopy(entries)
|
||||||
@ -926,7 +1006,15 @@ def publisher_list():
|
|||||||
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
|
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
|
||||||
.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(text('books_publishers_link.publisher')).order_by(order).all()
|
.group_by(text('books_publishers_link.publisher')).order_by(order).all()
|
||||||
char_list = generate_char_list(db.Publishers.name, db.books_publishers_link)
|
no_publisher_count = (calibre_db.session.query(db.Books)
|
||||||
|
.outerjoin(db.books_publishers_link).outerjoin(db.Publishers)
|
||||||
|
.filter(db.Publishers.name == None)
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
|
.count())
|
||||||
|
if no_publisher_count:
|
||||||
|
entries.append([db.Category(_("None"), "-1"), no_publisher_count])
|
||||||
|
entries = sorted(entries, key=lambda x: x[0].name, reverse=not order_no)
|
||||||
|
char_list = generate_char_list(entries)
|
||||||
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
||||||
title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
|
title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
|
||||||
else:
|
else:
|
||||||
@ -943,11 +1031,19 @@ def series_list():
|
|||||||
else:
|
else:
|
||||||
order = db.Series.sort.asc()
|
order = db.Series.sort.asc()
|
||||||
order_no = 1
|
order_no = 1
|
||||||
char_list = generate_char_list(db.Series.sort, db.books_series_link)
|
char_list = query_char_list(db.Series.sort, db.books_series_link)
|
||||||
if current_user.get_view_property('series', 'series_view') == 'list':
|
if current_user.get_view_property('series', 'series_view') == 'list':
|
||||||
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
|
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
|
||||||
.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(text('books_series_link.series')).order_by(order).all()
|
.group_by(text('books_series_link.series')).order_by(order).all()
|
||||||
|
no_series_count = (calibre_db.session.query(db.Books)
|
||||||
|
.outerjoin(db.books_series_link).outerjoin(db.Series)
|
||||||
|
.filter(db.Series.name == None)
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
|
.count())
|
||||||
|
if no_series_count:
|
||||||
|
entries.append([db.Category(_("None"), "-1"), no_series_count])
|
||||||
|
entries = sorted(entries, key=lambda x: x[0].name, reverse=not order_no)
|
||||||
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
||||||
title=_(u"Series"), page="serieslist", data="series", order=order_no)
|
title=_(u"Series"), page="serieslist", data="series", order=order_no)
|
||||||
else:
|
else:
|
||||||
@ -976,6 +1072,13 @@ def ratings_list():
|
|||||||
(db.Ratings.rating / 2).label('name')) \
|
(db.Ratings.rating / 2).label('name')) \
|
||||||
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
|
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
|
||||||
.group_by(text('books_ratings_link.rating')).order_by(order).all()
|
.group_by(text('books_ratings_link.rating')).order_by(order).all()
|
||||||
|
no_rating_count = (calibre_db.session.query(db.Books)
|
||||||
|
.outerjoin(db.books_ratings_link).outerjoin(db.Ratings)
|
||||||
|
.filter(db.Ratings.rating == None)
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
|
.count())
|
||||||
|
entries.append([db.Category(_("None"), "-1", -1), no_rating_count])
|
||||||
|
entries = sorted(entries, key=lambda x: x[0].rating, reverse=not order_no)
|
||||||
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
|
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
|
||||||
title=_(u"Ratings list"), page="ratingslist", data="ratings", order=order_no)
|
title=_(u"Ratings list"), page="ratingslist", data="ratings", order=order_no)
|
||||||
else:
|
else:
|
||||||
@ -997,6 +1100,12 @@ def formats_list():
|
|||||||
db.Data.format.label('format')) \
|
db.Data.format.label('format')) \
|
||||||
.join(db.Books).filter(calibre_db.common_filters()) \
|
.join(db.Books).filter(calibre_db.common_filters()) \
|
||||||
.group_by(db.Data.format).order_by(order).all()
|
.group_by(db.Data.format).order_by(order).all()
|
||||||
|
no_format_count = (calibre_db.session.query(db.Books).outerjoin(db.Data)
|
||||||
|
.filter(db.Data.format == None)
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
|
.count())
|
||||||
|
if no_format_count:
|
||||||
|
entries.append([db.Category(_("None"), "-1"), no_format_count])
|
||||||
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
|
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
|
||||||
title=_(u"File formats list"), page="formatslist", data="formats", order=order_no)
|
title=_(u"File formats list"), page="formatslist", data="formats", order=order_no)
|
||||||
else:
|
else:
|
||||||
@ -1008,15 +1117,10 @@ def formats_list():
|
|||||||
def language_overview():
|
def language_overview():
|
||||||
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == u"all":
|
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == u"all":
|
||||||
order_no = 0 if current_user.get_view_property('language', 'dir') == 'desc' else 1
|
order_no = 0 if current_user.get_view_property('language', 'dir') == 'desc' else 1
|
||||||
char_list = list()
|
|
||||||
languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
|
languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
|
||||||
for lang in languages:
|
char_list = generate_char_list(languages)
|
||||||
upper_lang = lang[0].name[0].upper()
|
return render_title_template('list.html', entries=languages, folder='web.books_list', charlist=char_list,
|
||||||
if upper_lang not in char_list:
|
title=_(u"Languages"), page="langlist", data="language", order=order_no)
|
||||||
char_list.append(upper_lang)
|
|
||||||
return render_title_template('languages.html', languages=languages,
|
|
||||||
charlist=char_list, title=_(u"Languages"), page="langlist",
|
|
||||||
data="language", order=order_no)
|
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@ -1034,7 +1138,15 @@ def category_list():
|
|||||||
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
|
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
|
||||||
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
|
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
|
||||||
.group_by(text('books_tags_link.tag')).all()
|
.group_by(text('books_tags_link.tag')).all()
|
||||||
char_list = generate_char_list(db.Tags.name, db.books_tags_link)
|
no_tag_count = (calibre_db.session.query(db.Books)
|
||||||
|
.outerjoin(db.books_tags_link).outerjoin(db.Tags)
|
||||||
|
.filter(db.Tags.name == None)
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
|
.count())
|
||||||
|
if no_tag_count:
|
||||||
|
entries.append([db.Category(_("None"), "-1"), no_tag_count])
|
||||||
|
entries = sorted(entries, key=lambda x: x[0].name, reverse=not order_no)
|
||||||
|
char_list = generate_char_list(entries)
|
||||||
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
|
||||||
title=_(u"Categories"), page="catlist", data="category", order=order_no)
|
title=_(u"Categories"), page="catlist", data="category", order=order_no)
|
||||||
else:
|
else:
|
||||||
|
1275
messages.pot
1275
messages.pot
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
# GDrive Integration
|
# GDrive Integration
|
||||||
google-api-python-client>=1.7.11,<2.44.0
|
google-api-python-client>=1.7.11,<2.46.0
|
||||||
gevent>20.6.0,<22.0.0
|
gevent>20.6.0,<22.0.0
|
||||||
greenlet>=0.4.17,<1.2.0
|
greenlet>=0.4.17,<1.2.0
|
||||||
httplib2>=0.9.2,<0.21.0
|
httplib2>=0.9.2,<0.21.0
|
||||||
@ -13,7 +13,7 @@ rsa>=3.4.2,<4.9.0
|
|||||||
|
|
||||||
# Gmail
|
# Gmail
|
||||||
google-auth-oauthlib>=0.4.3,<0.6.0
|
google-auth-oauthlib>=0.4.3,<0.6.0
|
||||||
google-api-python-client>=1.7.11,<2.44.0
|
google-api-python-client>=1.7.11,<2.46.0
|
||||||
|
|
||||||
# goodreads
|
# goodreads
|
||||||
goodreads>=0.3.2,<0.4.0
|
goodreads>=0.3.2,<0.4.0
|
||||||
|
@ -2,7 +2,7 @@ APScheduler>=3.6.3,<3.10.0
|
|||||||
werkzeug<2.1.0
|
werkzeug<2.1.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
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.6.1
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
backports_abc>=0.4
|
backports_abc>=0.4
|
||||||
Flask>=1.0.2,<2.1.0
|
Flask>=1.0.2,<2.1.0
|
||||||
|
@ -42,7 +42,7 @@ install_requires =
|
|||||||
werkzeug<2.1.0
|
werkzeug<2.1.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
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.6.1
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
backports_abc>=0.4
|
backports_abc>=0.4
|
||||||
Flask>=1.0.2,<2.1.0
|
Flask>=1.0.2,<2.1.0
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user