Fix for flask limiter 4.0
Fix reseting flask_limiter keys
This commit is contained in:
82
cps/kobo.py
82
cps/kobo.py
@@ -19,38 +19,30 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from functools import wraps
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
import json
|
import json
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
import requests
|
||||||
|
|
||||||
from flask import (
|
from flask import Blueprint, request, make_response, jsonify, current_app, url_for, redirect, abort, g
|
||||||
Blueprint,
|
|
||||||
request,
|
|
||||||
make_response,
|
|
||||||
jsonify,
|
|
||||||
current_app,
|
|
||||||
url_for,
|
|
||||||
redirect,
|
|
||||||
abort
|
|
||||||
)
|
|
||||||
from .cw_login import current_user
|
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
from sqlalchemy.exc import StatementError
|
from sqlalchemy.exc import StatementError
|
||||||
import requests
|
from flask_limiter.util import get_remote_address
|
||||||
|
|
||||||
|
from .cw_login import current_user, login_user
|
||||||
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 . import isoLanguages
|
|
||||||
from .epub import get_epub_layout
|
from .epub import get_epub_layout
|
||||||
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
|
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
|
||||||
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
|
||||||
from .kobo_auth import requires_kobo_auth, get_auth_token
|
from . import isoLanguages, limiter
|
||||||
|
|
||||||
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||||
@@ -65,6 +57,33 @@ kobo_auth.register_url_value_preprocessor(kobo)
|
|||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token():
|
||||||
|
if "auth_token" in g:
|
||||||
|
return g.get("auth_token")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def requires_kobo_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
auth_token = get_auth_token()
|
||||||
|
if auth_token is not None:
|
||||||
|
user = (
|
||||||
|
ub.session.query(ub.User)
|
||||||
|
.join(ub.RemoteAuthToken)
|
||||||
|
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
login_user(user)
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
log.debug("Received Kobo request without a recognizable auth token.")
|
||||||
|
return abort(401)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def get_store_url_for_current_request():
|
def get_store_url_for_current_request():
|
||||||
# Programmatically modify the current url to point to the official Kobo store
|
# Programmatically modify the current url to point to the official Kobo store
|
||||||
__, __, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
|
__, __, request_path_with_auth_token = request.full_path.rpartition("/kobo/")
|
||||||
@@ -140,7 +159,7 @@ def convert_to_kobo_timestamp_string(timestamp):
|
|||||||
|
|
||||||
@kobo.route("/v1/library/sync")
|
@kobo.route("/v1/library/sync")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# @download_required
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleSyncRequest():
|
def HandleSyncRequest():
|
||||||
if not current_user.role_download():
|
if not current_user.role_download():
|
||||||
log.info("Users need download permissions for syncing library to Kobo reader")
|
log.info("Users need download permissions for syncing library to Kobo reader")
|
||||||
@@ -336,6 +355,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
|||||||
@kobo.route("/v1/library/<book_uuid>/metadata")
|
@kobo.route("/v1/library/<book_uuid>/metadata")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@download_required
|
@download_required
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleMetadataRequest(book_uuid):
|
def HandleMetadataRequest(book_uuid):
|
||||||
if not current_app.wsgi_app.is_proxied:
|
if not current_app.wsgi_app.is_proxied:
|
||||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||||
@@ -368,7 +388,7 @@ def get_download_url_for_book(book_id, book_format):
|
|||||||
)
|
)
|
||||||
return url_for(
|
return url_for(
|
||||||
"kobo.download_book",
|
"kobo.download_book",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_id=book_id,
|
book_id=book_id,
|
||||||
book_format=book_format.lower(),
|
book_format=book_format.lower(),
|
||||||
_external=True,
|
_external=True,
|
||||||
@@ -503,9 +523,11 @@ 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
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
def HandleTagCreate():
|
def HandleTagCreate():
|
||||||
# catch delete requests, otherwise they are handled in the book delete handler
|
# catch delete requests, otherwise they are handled in the book delete handler
|
||||||
@@ -541,6 +563,7 @@ def HandleTagCreate():
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleTagUpdate(tag_id):
|
def HandleTagUpdate(tag_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
|
||||||
ub.Shelf.user_id == current_user.id).one_or_none()
|
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||||
@@ -595,6 +618,7 @@ def add_items_to_shelf(items, shelf):
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleTagAddItem(tag_id):
|
def HandleTagAddItem(tag_id):
|
||||||
items = None
|
items = None
|
||||||
try:
|
try:
|
||||||
@@ -625,6 +649,7 @@ def HandleTagAddItem(tag_id):
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleTagRemoveItem(tag_id):
|
def HandleTagRemoveItem(tag_id):
|
||||||
items = None
|
items = None
|
||||||
try:
|
try:
|
||||||
@@ -758,6 +783,7 @@ def create_kobo_tag(shelf):
|
|||||||
@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
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleStateRequest(book_uuid):
|
def HandleStateRequest(book_uuid):
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
if not book or not book.data:
|
if not book or not book.data:
|
||||||
@@ -907,6 +933,7 @@ def get_current_bookmark_response(current_bookmark):
|
|||||||
@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
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale):
|
||||||
try:
|
try:
|
||||||
if int(height) > 1000:
|
if int(height) > 1000:
|
||||||
@@ -943,6 +970,7 @@ def TopLevelEndpoint():
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleBookDeletionRequest(book_uuid):
|
def HandleBookDeletionRequest(book_uuid):
|
||||||
log.info("Kobo book delete request received for book %s" % book_uuid)
|
log.info("Kobo book delete request received for book %s" % book_uuid)
|
||||||
book = calibre_db.get_book_by_uuid(book_uuid)
|
book = calibre_db.get_book_by_uuid(book_uuid)
|
||||||
@@ -962,6 +990,7 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
||||||
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
||||||
def HandleUnimplementedRequest(dummy=None):
|
def HandleUnimplementedRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
||||||
request.base_url)
|
request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
@@ -976,6 +1005,9 @@ def HandleUnimplementedRequest(dummy=None):
|
|||||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/assets", methods=["GET"])
|
@kobo.route("/v1/assets", methods=["GET"])
|
||||||
def HandleUserRequest(dummy=None):
|
def HandleUserRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
|
log.error("Key: {}".format(limiter.current_limit.key))
|
||||||
|
log.error("Remaining: {}".format(limiter.current_limit.remaining))
|
||||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
@@ -983,6 +1015,7 @@ def HandleUserRequest(dummy=None):
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||||
def handle_benefits():
|
def handle_benefits():
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
else:
|
else:
|
||||||
@@ -992,6 +1025,7 @@ def handle_benefits():
|
|||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||||
def handle_getests():
|
def handle_getests():
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
else:
|
else:
|
||||||
@@ -1018,6 +1052,7 @@ def handle_getests():
|
|||||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||||
def HandleProductsRequest(dummy=None):
|
def HandleProductsRequest(dummy=None):
|
||||||
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
||||||
request.base_url)
|
request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
@@ -1046,7 +1081,10 @@ def make_calibre_web_auth_response():
|
|||||||
@kobo.route("/v1/auth/refresh", methods=["POST"])
|
@kobo.route("/v1/auth/refresh", methods=["POST"])
|
||||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleAuthRequest():
|
def HandleAuthRequest():
|
||||||
|
log.error(limiter.current_limit)
|
||||||
|
log.error(limiter.current_limit)
|
||||||
log.debug('Kobo Auth request')
|
log.debug('Kobo Auth request')
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
try:
|
try:
|
||||||
@@ -1058,6 +1096,7 @@ def HandleAuthRequest():
|
|||||||
|
|
||||||
@kobo.route("/v1/initialization")
|
@kobo.route("/v1/initialization")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def HandleInitRequest():
|
def HandleInitRequest():
|
||||||
log.info('Init')
|
log.info('Init')
|
||||||
|
|
||||||
@@ -1088,7 +1127,7 @@ def HandleInitRequest():
|
|||||||
kobo_resources["image_host"] = calibre_web_url
|
kobo_resources["image_host"] = calibre_web_url
|
||||||
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
||||||
url_for("kobo.HandleCoverImageRequest",
|
url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1096,7 +1135,7 @@ def HandleInitRequest():
|
|||||||
isGreyscale='isGreyscale'))
|
isGreyscale='isGreyscale'))
|
||||||
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
||||||
url_for("kobo.HandleCoverImageRequest",
|
url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1104,7 +1143,7 @@ def HandleInitRequest():
|
|||||||
else:
|
else:
|
||||||
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
||||||
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1112,7 +1151,7 @@ def HandleInitRequest():
|
|||||||
isGreyscale='isGreyscale',
|
isGreyscale='isGreyscale',
|
||||||
_external=True))
|
_external=True))
|
||||||
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||||
auth_token=kobo_auth.get_auth_token(),
|
auth_token=get_auth_token(),
|
||||||
book_uuid="{ImageId}",
|
book_uuid="{ImageId}",
|
||||||
width="{width}",
|
width="{width}",
|
||||||
height="{height}",
|
height="{height}",
|
||||||
@@ -1128,6 +1167,7 @@ def HandleInitRequest():
|
|||||||
@kobo.route("/download/<book_id>/<book_format>")
|
@kobo.route("/download/<book_id>/<book_format>")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@download_required
|
@download_required
|
||||||
|
# @limiter.limit("3/minute", key_func=get_remote_address)
|
||||||
def download_book(book_id, book_format):
|
def download_book(book_id, book_format):
|
||||||
return get_download_link(book_id, book_format, "kobo")
|
return get_download_link(book_id, book_format, "kobo")
|
||||||
|
|
||||||
@@ -1299,3 +1339,5 @@ def NATIVE_KOBO_RESOURCES():
|
|||||||
"userguide_host": "https://ereaderfiles.kobo.com",
|
"userguide_host": "https://ereaderfiles.kobo.com",
|
||||||
"wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist"
|
"wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||||
|
|||||||
@@ -62,14 +62,12 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
|
|||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from flask import g, Blueprint, abort, request
|
from flask import g, Blueprint, request
|
||||||
from .cw_login import login_user, current_user
|
from .cw_login import current_user
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_limiter import RateLimitExceeded
|
|
||||||
|
|
||||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .usermanagement import user_login_required
|
from .usermanagement import user_login_required
|
||||||
|
|
||||||
@@ -135,13 +133,6 @@ def disable_failed_auth_redirect_for_blueprint(bp):
|
|||||||
lm.blueprint_login_views[bp.name] = None
|
lm.blueprint_login_views[bp.name] = None
|
||||||
|
|
||||||
|
|
||||||
def get_auth_token():
|
|
||||||
if "auth_token" in g:
|
|
||||||
return g.get("auth_token")
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def register_url_value_preprocessor(kobo):
|
def register_url_value_preprocessor(kobo):
|
||||||
@kobo.url_value_preprocessor
|
@kobo.url_value_preprocessor
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
@@ -149,28 +140,3 @@ def register_url_value_preprocessor(kobo):
|
|||||||
g.auth_token = values.pop("auth_token")
|
g.auth_token = values.pop("auth_token")
|
||||||
|
|
||||||
|
|
||||||
def requires_kobo_auth(f):
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
auth_token = get_auth_token()
|
|
||||||
if auth_token is not None:
|
|
||||||
#try:
|
|
||||||
# limiter.check()
|
|
||||||
#except RateLimitExceeded:
|
|
||||||
# return abort(429)
|
|
||||||
#except (ConnectionError, Exception) as e:
|
|
||||||
# log.error("Connection error to limiter backend: %s", e)
|
|
||||||
# return abort(429)
|
|
||||||
user = (
|
|
||||||
ub.session.query(ub.User)
|
|
||||||
.join(ub.RemoteAuthToken)
|
|
||||||
.filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if user is not None:
|
|
||||||
login_user(user)
|
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
log.debug("Received Kobo request without a recognizable auth token.")
|
|
||||||
return abort(401)
|
|
||||||
return inner
|
|
||||||
|
|||||||
@@ -46,11 +46,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
from .kobo import kobo, get_kobo_activated
|
from .kobo import kobo, get_kobo_activated
|
||||||
from .kobo_auth import kobo_auth
|
from .kobo_auth import kobo_auth
|
||||||
from flask_limiter.util import get_remote_address
|
|
||||||
kobo_available = get_kobo_activated()
|
kobo_available = get_kobo_activated()
|
||||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||||
kobo_available = False
|
kobo_available = False
|
||||||
kobo = kobo_auth = get_remote_address = None
|
kobo = kobo_auth = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .oauth_bb import oauth
|
from .oauth_bb import oauth
|
||||||
@@ -77,7 +76,6 @@ def main():
|
|||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
|
||||||
app.register_blueprint(kobo)
|
app.register_blueprint(kobo)
|
||||||
app.register_blueprint(kobo_auth)
|
app.register_blueprint(kobo_auth)
|
||||||
if oauth_available:
|
if oauth_available:
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ def verify_password(username, password):
|
|||||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||||
login_result, error = services.ldap.bind_user(user.name, password)
|
login_result, error = services.ldap.bind_user(user.name, password)
|
||||||
if login_result:
|
if login_result:
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
return user
|
return user
|
||||||
if error is not None:
|
if error is not None:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
else:
|
else:
|
||||||
# limiter.check()
|
# limiter.check()
|
||||||
if check_password_hash(str(user.password), password):
|
if check_password_hash(str(user.password), password):
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
return user
|
return user
|
||||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
||||||
@@ -120,7 +120,7 @@ def load_user_from_reverse_proxy_header(req):
|
|||||||
if rp_header_username:
|
if rp_header_username:
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
||||||
if user:
|
if user:
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
24
cps/web.py
24
cps/web.py
@@ -30,7 +30,6 @@ from flask import session as flask_session
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_babel import get_locale
|
from flask_babel import get_locale
|
||||||
from .cw_login import login_user, logout_user, current_user
|
from .cw_login import login_user, logout_user, current_user
|
||||||
# from flask_limiter import RateLimitExceeded
|
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
|
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
|
||||||
@@ -39,7 +38,7 @@ from sqlalchemy.sql.functions import coalesce
|
|||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
from . import constants, logger, isoLanguages, services
|
from . import constants, logger, isoLanguages, services, limiter
|
||||||
from . import db, ub, config, app
|
from . import db, ub, config, app
|
||||||
from . import calibre_db, kobo_sync_status
|
from . import calibre_db, kobo_sync_status
|
||||||
from .search import render_search_results, render_adv_search_results
|
from .search import render_search_results, render_adv_search_results
|
||||||
@@ -55,7 +54,6 @@ from .usermanagement import login_required_if_no_ano
|
|||||||
from .kobo_sync_status import remove_synced_book
|
from .kobo_sync_status import remove_synced_book
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
from .kobo_sync_status import change_archived_books
|
from .kobo_sync_status import change_archived_books
|
||||||
from . import limiter
|
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks_status import render_task_status
|
from .tasks_status import render_task_status
|
||||||
from .usermanagement import user_login_required
|
from .usermanagement import user_login_required
|
||||||
@@ -1285,15 +1283,6 @@ def register_post():
|
|||||||
if not config.config_public_reg:
|
if not config.config_public_reg:
|
||||||
abort(404)
|
abort(404)
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
#try:
|
|
||||||
# limiter.check()
|
|
||||||
#except RateLimitExceeded:
|
|
||||||
# flash(_(u"Please wait one minute to register next user"), category="error")
|
|
||||||
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
|
||||||
#except (ConnectionError, Exception) as e:
|
|
||||||
# log.error("Connection error to limiter backend: %s", e)
|
|
||||||
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
|
||||||
# return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not config.get_mail_server_configured():
|
if not config.get_mail_server_configured():
|
||||||
@@ -1358,7 +1347,7 @@ def register():
|
|||||||
def handle_login_user(user, remember, message, category):
|
def handle_login_user(user, remember, message, category):
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
flash(message, category=category)
|
flash(message, category=category)
|
||||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||||
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||||
|
|
||||||
|
|
||||||
@@ -1392,15 +1381,6 @@ def login():
|
|||||||
def login_post():
|
def login_post():
|
||||||
form = request.form.to_dict()
|
form = request.form.to_dict()
|
||||||
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
|
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
|
||||||
#try:
|
|
||||||
# limiter.check()
|
|
||||||
#except RateLimitExceeded:
|
|
||||||
# flash(_("Please wait one minute before next login"), category="error")
|
|
||||||
# return render_login(username, form.get("password", ""))
|
|
||||||
#except (ConnectionError, Exception) as e:
|
|
||||||
# log.error("Connection error to limiter backend: %s", e)
|
|
||||||
# flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
|
|
||||||
# return render_login(username, form.get("password", ""))
|
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
||||||
|
|||||||
@@ -33,26 +33,26 @@ dependencies = [
|
|||||||
"Flask>=1.0.2,<3.2.0",
|
"Flask>=1.0.2,<3.2.0",
|
||||||
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
||||||
"pycountry>=20.0.0,<25.0.0;python_version>='3.12'",
|
"pycountry>=20.0.0,<25.0.0;python_version>='3.12'",
|
||||||
"PyPDF>=6.1.3,<6.5.0",
|
"PyPDF>=6.1.3,<6.7.0",
|
||||||
"pytz>=2016.10",
|
"pytz>=2016.10",
|
||||||
"requests>=2.32.0,<2.33.0",
|
"requests>=2.32.0,<2.33.0",
|
||||||
"SQLAlchemy>=1.3.0,<2.1.0",
|
"SQLAlchemy>=1.3.0,<2.1.0",
|
||||||
"tornado>=6.4.2,<6.6",
|
"tornado>=6.4.2,<6.6",
|
||||||
"Wand>=0.4.4,<0.7.0",
|
"Wand>=0.4.4,<0.7.0",
|
||||||
"unidecode>=0.04.19,<1.4.0",
|
"unidecode>=0.04.19,<1.5.0",
|
||||||
"lxml>=4.9.1,<5.4.0",
|
"lxml>=4.9.1,<5.4.0",
|
||||||
"flask-wtf>=0.14.2,<1.3.0",
|
"flask-wtf>=0.14.2,<1.3.0",
|
||||||
"chardet>=3.0.0,<5.3.0",
|
"chardet>=3.0.0,<5.3.0",
|
||||||
"netifaces-plus>=0.12.0,<0.13.0",
|
"netifaces-plus>=0.12.0,<0.13.0",
|
||||||
"urllib3>=1.22,<3.0",
|
"urllib3>=1.22,<3.0",
|
||||||
"Flask-Limiter>=2.3.0,<3.13.0",
|
"Flask-Limiter>=2.3.0,<4.2.0",
|
||||||
"regex>=2022.3.2,<2025.3.20",
|
"regex>=2022.3.2,<2026.1.16",
|
||||||
"bleach>=6.0.0,<6.3.0",
|
"bleach>=6.0.0,<6.4.0",
|
||||||
"python-magic>=0.4.27,<0.5.0",
|
"python-magic>=0.4.27,<0.5.0",
|
||||||
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
||||||
"flask-httpAuth>=4.4.0,<5.0.0",
|
"flask-httpAuth>=4.4.0,<5.0.0",
|
||||||
"cryptography>=39.0.0,<45.0.0",
|
"cryptography>=39.0.0,<47.0.0",
|
||||||
"certifi>=2024.7.4,<2025.8.24",
|
"certifi>=2024.7.4,<2026.1.5",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
@@ -70,9 +70,9 @@ content-type = "text/markdown"
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
gdrive = [
|
gdrive = [
|
||||||
"google-api-python-client>=2.73.00,<2.200.0",
|
"google-api-python-client>=2.73.00,<2.200.0",
|
||||||
"gevent>20.6.0,<24.12.0",
|
"gevent>20.6.0,<25.9.2",
|
||||||
"greenlet>=0.4.17,<3.3.0",
|
"greenlet>=0.4.17,<3.4.0",
|
||||||
"httplib2>=0.9.2,<0.23.0",
|
"httplib2>=0.9.2,<0.32.0",
|
||||||
"oauth2client>=4.0.0,<4.1.4",
|
"oauth2client>=4.0.0,<4.1.4",
|
||||||
"uritemplate>=3.0.0,<4.3.0",
|
"uritemplate>=3.0.0,<4.3.0",
|
||||||
"pyasn1-modules>=0.0.8,<0.7.0",
|
"pyasn1-modules>=0.0.8,<0.7.0",
|
||||||
@@ -91,19 +91,19 @@ goodreads = [
|
|||||||
]
|
]
|
||||||
ldap = [
|
ldap = [
|
||||||
"python-ldap>=3.0.0,<3.5.0",
|
"python-ldap>=3.0.0,<3.5.0",
|
||||||
"Flask-SimpleLDAP>=1.4.0,<2.1.0",
|
"Flask-SimpleLDAP>=1.4.0,<2.2.0",
|
||||||
]
|
]
|
||||||
oauth = [
|
oauth = [
|
||||||
"Flask-Dance>=2.0.0,<7.2.0",
|
"Flask-Dance>=2.0.0,<7.2.0",
|
||||||
"SQLAlchemy-Utils>=0.33.5,<0.42.0",
|
"SQLAlchemy-Utils>=0.33.5,<0.43.0",
|
||||||
]
|
]
|
||||||
metadata = [
|
metadata = [
|
||||||
"rarfile>=3.2,<5.0",
|
"rarfile>=3.2,<5.0",
|
||||||
"scholarly>=1.2.0,<1.8",
|
"scholarly>=1.2.0,<1.8",
|
||||||
"markdown2>=2.0.0,<2.6.0",
|
"markdown2>=2.0.0,<2.6.0",
|
||||||
"html2text>=2020.1.16,<2025.2.26",
|
"html2text>=2020.1.16,<2025.4.16",
|
||||||
"python-dateutil>=2.1,<2.10.0",
|
"python-dateutil>=2.1,<2.10.0",
|
||||||
"beautifulsoup4>=4.0.1,<4.14.0",
|
"beautifulsoup4>=4.0.1,<4.15.0",
|
||||||
"faust-cchardet>=2.1.18,<2.1.20",
|
"faust-cchardet>=2.1.18,<2.1.20",
|
||||||
"py7zr>=0.15.0,<0.21.0",
|
"py7zr>=0.15.0,<0.21.0",
|
||||||
"mutagen>=1.40.0,<1.50.0",
|
"mutagen>=1.40.0,<1.50.0",
|
||||||
|
|||||||
@@ -37,20 +37,20 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
|
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
|
||||||
|
|
||||||
<p class='text-justify attribute'><strong>Start Time: </strong>2026-01-24 20:50:12</p>
|
<p class='text-justify attribute'><strong>Start Time: </strong>2026-02-17 19:21:22</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
||||||
|
|
||||||
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-01-25 04:15:44</p>
|
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-02-18 02:50:40</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
<div class="col-xs-6 col-md-6 col-sm-offset-3">
|
||||||
<p class='text-justify attribute'><strong>Duration: </strong>6h 13 min</p>
|
<p class='text-justify attribute'><strong>Duration: </strong>6h 14 min</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2039,13 +2039,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="su" class="errorClass">
|
<tr id="su" class="failClass">
|
||||||
<td>TestLoadMetadata</td>
|
<td>TestLoadMetadata</td>
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">1</td>
|
||||||
<td class="text-center">0</td>
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">0</td>
|
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">1</td>
|
||||||
<td class="text-center">0</td>
|
<td class="text-center">0</td>
|
||||||
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a onclick="showClassDetail('c18', 1)">Detail</a>
|
<a onclick="showClassDetail('c18', 1)">Detail</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -2053,27 +2053,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="et18.1" class="none bg-info">
|
<tr id="ft18.1" class="none bg-danger">
|
||||||
<td>
|
<td>
|
||||||
<div class='testcase'>TestLoadMetadata - test_load_metadata</div>
|
<div class='testcase'>TestLoadMetadata - test_load_metadata</div>
|
||||||
</td>
|
</td>
|
||||||
<td colspan='6'>
|
<td colspan='6'>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et18.1')">ERROR</a>
|
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft18.1')">FAIL</a>
|
||||||
</div>
|
</div>
|
||||||
<!--css div popup start-->
|
<!--css div popup start-->
|
||||||
<div id="div_et18.1" class="popup_window test_output" style="display:block;">
|
<div id="div_ft18.1" class="popup_window test_output" style="display:block;">
|
||||||
<div class='close_button pull-right'>
|
<div class='close_button pull-right'>
|
||||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
||||||
onclick="document.getElementById('div_et18.1').style.display='none'"><span
|
onclick="document.getElementById('div_ft18.1').style.display='none'"><span
|
||||||
aria-hidden="true">×</span></button>
|
aria-hidden="true">×</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-left pull-left">
|
<div class="text-left pull-left">
|
||||||
<pre class="text-left">Traceback (most recent call last):
|
<pre class="text-left">Traceback (most recent call last):
|
||||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 87, in test_load_metadata
|
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 220, in test_load_metadata
|
||||||
if results[cont]['source'] == 'https://comicvine.gamespot.com/':
|
self.assertLessEqual(diff(BytesIO(cover), BytesIO(new_cover), delete_diff_file=True), 0.025)
|
||||||
~~~~~~~^^^^^^
|
AssertionError: 0.04218017335689641 not less than or equal to 0.025</pre>
|
||||||
IndexError: list index out of range</pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2270,13 +2269,13 @@ IndexError: list index out of range</pre>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="su" class="failClass">
|
<tr id="su" class="passClass">
|
||||||
<td>TestLoadMetadataScholar</td>
|
<td>TestLoadMetadataScholar</td>
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">1</td>
|
||||||
<td class="text-center">0</td>
|
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">1</td>
|
||||||
<td class="text-center">0</td>
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">0</td>
|
<td class="text-center">0</td>
|
||||||
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a onclick="showClassDetail('c20', 1)">Detail</a>
|
<a onclick="showClassDetail('c20', 1)">Detail</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -2284,31 +2283,11 @@ IndexError: list index out of range</pre>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="ft20.1" class="none bg-danger">
|
<tr id='pt20.1' class='hiddenRow bg-success'>
|
||||||
<td>
|
<td>
|
||||||
<div class='testcase'>TestLoadMetadataScholar - test_load_metadata</div>
|
<div class='testcase'>TestLoadMetadataScholar - test_load_metadata</div>
|
||||||
</td>
|
</td>
|
||||||
<td colspan='6'>
|
<td colspan='6' align='center'>PASS</td>
|
||||||
<div class="text-center">
|
|
||||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft20.1')">FAIL</a>
|
|
||||||
</div>
|
|
||||||
<!--css div popup start-->
|
|
||||||
<div id="div_ft20.1" class="popup_window test_output" style="display:block;">
|
|
||||||
<div class='close_button pull-right'>
|
|
||||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
|
||||||
onclick="document.getElementById('div_ft20.1').style.display='none'"><span
|
|
||||||
aria-hidden="true">×</span></button>
|
|
||||||
</div>
|
|
||||||
<div class="text-left pull-left">
|
|
||||||
<pre class="text-left">Traceback (most recent call last):
|
|
||||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_metadata_scholar.py", line 74, in test_load_metadata
|
|
||||||
self.assertEqual(30, len(results))
|
|
||||||
AssertionError: 30 != 20</pre>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
<!--css div popup end-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
@@ -4684,11 +4663,11 @@ AssertionError: 30 != 20</pre>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="su" class="failClass">
|
<tr id="su" class="skipClass">
|
||||||
<td>TestThumbnails</td>
|
<td>TestThumbnails</td>
|
||||||
<td class="text-center">8</td>
|
<td class="text-center">8</td>
|
||||||
<td class="text-center">6</td>
|
<td class="text-center">7</td>
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">0</td>
|
<td class="text-center">0</td>
|
||||||
<td class="text-center">1</td>
|
<td class="text-center">1</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -4761,31 +4740,11 @@ AssertionError: 30 != 20</pre>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr id="ft52.8" class="none bg-danger">
|
<tr id='pt52.8' class='hiddenRow bg-success'>
|
||||||
<td>
|
<td>
|
||||||
<div class='testcase'>TestThumbnails - test_sideloaded_book</div>
|
<div class='testcase'>TestThumbnails - test_sideloaded_book</div>
|
||||||
</td>
|
</td>
|
||||||
<td colspan='6'>
|
<td colspan='6' align='center'>PASS</td>
|
||||||
<div class="text-center">
|
|
||||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft52.8')">FAIL</a>
|
|
||||||
</div>
|
|
||||||
<!--css div popup start-->
|
|
||||||
<div id="div_ft52.8" class="popup_window test_output" style="display:block;">
|
|
||||||
<div class='close_button pull-right'>
|
|
||||||
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
|
|
||||||
onclick="document.getElementById('div_ft52.8').style.display='none'"><span
|
|
||||||
aria-hidden="true">×</span></button>
|
|
||||||
</div>
|
|
||||||
<div class="text-left pull-left">
|
|
||||||
<pre class="text-left">Traceback (most recent call last):
|
|
||||||
File "/home/ozzie/Development/calibre-web-test/test/test_thumbnails.py", line 317, in test_sideloaded_book
|
|
||||||
self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
|
|
||||||
AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182775 difference)</pre>
|
|
||||||
</div>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
</div>
|
|
||||||
<!--css div popup end-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
@@ -5992,9 +5951,9 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
<tr id='total_row' class="text-center bg-grey">
|
<tr id='total_row' class="text-center bg-grey">
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td>538</td>
|
<td>538</td>
|
||||||
<td>528</td>
|
<td>530</td>
|
||||||
<td>2</td>
|
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
|
<td>0</td>
|
||||||
<td>7</td>
|
<td>7</td>
|
||||||
<td> </td>
|
<td> </td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -6023,7 +5982,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Platform</th>
|
<th>Platform</th>
|
||||||
<td>Linux 6.8.0-90-generic #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025 x86_64 x86_64</td>
|
<td>Linux 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64 x86_64</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6041,19 +6000,19 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>babel</th>
|
<th>babel</th>
|
||||||
<td>2.17.0</td>
|
<td>2.18.0</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>bleach</th>
|
<th>bleach</th>
|
||||||
<td>6.2.0</td>
|
<td>6.3.0</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>certifi</th>
|
<th>certifi</th>
|
||||||
<td>2025.8.3</td>
|
<td>2026.1.4</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6065,7 +6024,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>cryptography</th>
|
<th>cryptography</th>
|
||||||
<td>44.0.3</td>
|
<td>46.0.5</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6089,7 +6048,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Flask-Limiter</th>
|
<th>Flask-Limiter</th>
|
||||||
<td>3.12</td>
|
<td>4.1.1</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6137,7 +6096,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>pypdf</th>
|
<th>pypdf</th>
|
||||||
<td>6.4.2</td>
|
<td>6.6.2</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6155,7 +6114,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>regex</th>
|
<th>regex</th>
|
||||||
<td>2024.11.6</td>
|
<td>2026.1.15</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6179,7 +6138,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Unidecode</th>
|
<th>Unidecode</th>
|
||||||
<td>1.3.8</td>
|
<td>1.4.0</td>
|
||||||
<td>Basic</td>
|
<td>Basic</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6203,7 +6162,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestBackupMetadataGdrive</td>
|
<td>TestBackupMetadataGdrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6233,7 +6192,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestCliGdrivedb</td>
|
<td>TestCliGdrivedb</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6263,7 +6222,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestEbookConvertCalibreGDrive</td>
|
<td>TestEbookConvertCalibreGDrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6293,7 +6252,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestEbookConvertGDriveKepubify</td>
|
<td>TestEbookConvertGDriveKepubify</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6335,7 +6294,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestEditAuthorsGdrive</td>
|
<td>TestEditAuthorsGdrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6371,7 +6330,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestEditBooksOnGdrive</td>
|
<td>TestEditBooksOnGdrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6413,7 +6372,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestEmbedMetadataGdrive</td>
|
<td>TestEmbedMetadataGdrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6443,7 +6402,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>google-api-python-client</th>
|
<th>google-api-python-client</th>
|
||||||
<td>2.188.0</td>
|
<td>2.190.0</td>
|
||||||
<td>TestSetupGdrive</td>
|
<td>TestSetupGdrive</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6496,8 +6455,8 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Flask-SimpleLDAP</th>
|
<th>flask-simpleldap</th>
|
||||||
<td>2.0.0</td>
|
<td>2.1.0</td>
|
||||||
<td>TestLdapLogin</td>
|
<td>TestLdapLogin</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -6545,7 +6504,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
drawCircle(528, 2, 1, 7);
|
drawCircle(530, 1, 0, 7);
|
||||||
showCase(5);
|
showCase(5);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user